Android Push Notifications with Kotlin: A Step-by-Step Guide

android push notifications kotlin step step guide

Introduction

This tutorial will guide you through the process of implementing push notifications in an Android app using Kotlin. Push notifications are messages that are sent from a server to an app on a user's device. They can be used to deliver important updates, reminders, or promotions directly to the user's device, even when the app is not actively running. In this tutorial, we will cover the benefits of using push notifications in Android, the steps to set up the project, and the process of implementing and customizing push notifications using Kotlin.

What are push notifications?

Push notifications are messages that are sent from a server to an app on a user's device. They can be used to deliver important updates, reminders, or promotions directly to the user's device, even when the app is not actively running. Push notifications are commonly used in mobile apps to engage and re-engage users, increase user retention, and deliver personalized content.

Why use push notifications in Android?

There are several benefits to using push notifications in Android apps. Firstly, they allow you to deliver important information and updates to your users in real-time. This can be especially useful for apps that rely on timely information, such as news apps or weather apps. Push notifications also help to increase user engagement and retention by sending personalized and relevant content directly to the user's device. Additionally, push notifications can be used to re-engage users who have not used the app in a while by sending them reminders or promotions.

Benefits of using Kotlin for push notifications

Kotlin is a modern programming language that is fully interoperable with Java. It offers many benefits for Android app development, including improved code safety, conciseness, and null safety. When it comes to implementing push notifications, Kotlin provides a clean and expressive syntax, making the code easier to read and maintain. Kotlin's null safety features also help to prevent null pointer exceptions, which can be a common source of bugs in Android apps. Overall, using Kotlin for push notifications can lead to more robust and maintainable code.

Setting up the Project

Before we can start implementing push notifications in our Android app, we need to set up the project and configure Firebase Cloud Messaging (FCM). FCM is a cross-platform messaging solution that allows you to send push notifications to Android, iOS, and web apps.

Creating a new Android project

To create a new Android project, open Android Studio and select "Start a new Android Studio project" from the welcome screen. Follow the prompts to configure your project, including setting the project name, package name, and minimum SDK version. Once the project is created, you will see the project structure in the project explorer.

Adding necessary dependencies

To enable push notifications in our Android app, we need to add the necessary dependencies to the project. Open the build.gradle file for the app module and add the following dependencies:

implementation 'com.google.firebase:firebase-messaging:22.0.0'

This will add the Firebase Cloud Messaging dependency to our project, which is required for sending and receiving push notifications.

Configuring Firebase Cloud Messaging

To configure Firebase Cloud Messaging for our project, we need to connect our app to a Firebase project. If you don't have a Firebase project, you can create one by visiting the Firebase Console and clicking on "Add project". Once the project is created, follow the steps to connect your app to the Firebase project. This will involve downloading a google-services.json file and adding it to your project's app module.

Implementing Push Notifications

Now that we have set up the project and configured Firebase Cloud Messaging, we can start implementing push notifications in our Android app.

Registering the device for push notifications

To start receiving push notifications, we need to register the device with Firebase Cloud Messaging. This involves generating a unique token for the device and sending it to the server. In our app, we can register the device for push notifications by adding the following code to our main activity:

class MainActivity : AppCompatActivity() {

    private lateinit var firebaseMessaging: FirebaseMessaging

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        firebaseMessaging = FirebaseMessaging.getInstance()

        firebaseMessaging.token.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val token = task.result
                Log.d(TAG, "Token: $token")
                // Send token to server
            } else {
                Log.w(TAG, "Failed to get token")
            }
        }
    }

    companion object {
        private const val TAG = "MainActivity"
    }
}

In this code, we first initialize the firebaseMessaging instance by calling FirebaseMessaging.getInstance(). We then use the token property to generate a unique token for the device. The token is returned as a Task object, so we add a listener to the task to handle the result. If the task is successful, we retrieve the token from the result and log it. This token can then be sent to the server for further processing.

Handling token refresh

The token generated by Firebase Cloud Messaging can change over time, so it's important to handle token refresh. This can be done by implementing the FirebaseMessagingService class and overriding the onNewToken method. Add the following code to your project to handle token refresh:

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onNewToken(token: String) {
        super.onNewToken(token)
        Log.d(TAG, "Refreshed token: $token")
        // Send token to server
    }

    companion object {
        private const val TAG = "MyFirebaseMessagingService"
    }
}

In this code, we override the onNewToken method and log the refreshed token. This token can then be sent to the server for further processing.

Receiving and displaying push notifications

To receive and display push notifications in our app, we need to implement the FirebaseMessagingService class and override the onMessageReceived method. Add the following code to your project to handle push notifications:

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        Log.d(TAG, "Message data: ${remoteMessage.data}")
        // Display push notification
    }

    companion object {
        private const val TAG = "MyFirebaseMessagingService"
    }
}

In this code, we override the onMessageReceived method to handle incoming push notifications. We log the message data, which can include custom data sent from the server. We can then use this data to display a custom push notification in our app.

Customizing Push Notifications

Firebase Cloud Messaging allows us to customize the content and appearance of push notifications. We can add custom data to push notifications, create custom notification layouts, and handle notification actions.

Adding custom data to push notifications

To add custom data to push notifications, we can include additional key-value pairs in the data field of the notification payload sent from the server. This data can then be accessed in the onMessageReceived method of our FirebaseMessagingService implementation. For example, we can modify the onMessageReceived method to handle custom data as follows:

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        Log.d(TAG, "Message data: ${remoteMessage.data}")

        val title = remoteMessage.data["title"]
        val message = remoteMessage.data["message"]

        // Display push notification with custom data
    }

    companion object {
        private const val TAG = "MyFirebaseMessagingService"
    }
}

In this code, we access the custom data sent from the server by using the keys "title" and "message". We can then use this data to display a custom push notification in our app.

Creating custom notification layouts

To create custom notification layouts, we need to create a layout XML file in our app's res/layout directory. This layout file will define the structure and appearance of the custom notification. For example, we can create a layout file named custom_notification_layout.xml with the following content:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/notification_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_notification_icon" />

    <TextView
        android:id="@+id/notification_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.Notification.Title" />

    <TextView
        android:id="@+id/notification_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="@style/TextAppearance.Notification.Message" />

</LinearLayout>

In this layout file, we define a LinearLayout with vertical orientation. Inside the LinearLayout, we include an ImageView for the notification icon, and two TextViews for the notification title and message.

To use this custom layout for push notifications, we need to modify the onMessageReceived method of our FirebaseMessagingService implementation. Add the following code to handle custom notification layouts:

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        Log.d(TAG, "Message data: ${remoteMessage.data}")

        val title = remoteMessage.data["title"]
        val message = remoteMessage.data["message"]

        val notificationLayout = RemoteViews(packageName, R.layout.custom_notification_layout)
        notificationLayout.setTextViewText(R.id.notification_title, title)
        notificationLayout.setTextViewText(R.id.notification_message, message)

        val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification_icon)
            .setStyle(NotificationCompat.DecoratedCustomViewStyle())
            .setCustomContentView(notificationLayout)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)

        // Display custom push notification
    }

    companion object {
        private const val TAG = "MyFirebaseMessagingService"
        private const val CHANNEL_ID = "my_channel_id"
    }
}

In this code, we create a RemoteViews object by inflating the custom_notification_layout.xml file. We then set the text for the notification title and message using the custom data received from the server. Finally, we create a NotificationCompat.Builder object and set the custom content view using the setCustomContentView method. This will display the custom notification layout when the push notification is received.

Handling notification actions

Firebase Cloud Messaging allows us to include actions in push notifications, which can be triggered by the user. To handle notification actions, we need to create a PendingIntent for each action and set it using the addAction method of the NotificationCompat.Builder class. For example, we can modify the onMessageReceived method to handle notification actions as follows:

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        super.onMessageReceived(remoteMessage)
        Log.d(TAG, "Message data: ${remoteMessage.data}")

        val title = remoteMessage.data["title"]
        val message = remoteMessage.data["message"]

        val notificationLayout = RemoteViews(packageName, R.layout.custom_notification_layout)
        notificationLayout.setTextViewText(R.id.notification_title, title)
        notificationLayout.setTextViewText(R.id.notification_message, message)

        val dismissIntent = Intent(this, NotificationActionReceiver::class.java)
        dismissIntent.action = "DISMISS"
        val dismissPendingIntent = PendingIntent.getBroadcast(this, 0, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT)

        val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification_icon)
            .setStyle(NotificationCompat.DecoratedCustomViewStyle())
            .setCustomContentView(notificationLayout)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .addAction(R.drawable.ic_dismiss, "Dismiss", dismissPendingIntent)

        // Display push notification with action
    }

    companion object {
        private const val TAG = "MyFirebaseMessagingService"
        private const val CHANNEL_ID = "my_channel_id"
    }
}

In this code, we create a PendingIntent for the dismiss action by creating an Intent and setting its action to "DISMISS". We then create the PendingIntent using the PendingIntent.getBroadcast method. We can add additional actions by creating PendingIntent objects for each action and setting them using the addAction method of the NotificationCompat.Builder class. These actions can then be handled by creating a broadcast receiver and registering it in the manifest.

Handling Push Notification Errors

While implementing push notifications, it's important to handle potential error scenarios to ensure a smooth user experience. We need to handle device not registered, invalid server key, and network errors.

Handling device not registered

Sometimes, a device may not be registered for push notifications or may have uninstalled the app. In such cases, when sending a push notification to the device, we may receive a "NotRegistered" error. To handle this error, we can implement the FirebaseMessagingService class and override the onDeletedMessages method. Add the following code to handle device not registered errors:

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onDeletedMessages() {
        super.onDeletedMessages()
        Log.d(TAG, "Device not registered")
    }

    companion object {
        private const val TAG = "MyFirebaseMessagingService"
    }
}

In this code, we override the onDeletedMessages method to handle device not registered errors. We log the error message and can take any necessary action, such as removing the device token from the server.

Handling invalid server key

If the server key used to send push notifications is invalid or expired, we may receive an "InvalidServerKey" error. To handle this error, we can implement the FirebaseMessagingService class and override the onSendError method. Add the following code to handle invalid server key errors:

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onSendError(messageId: String, exception: Exception) {
        super.onSendError(messageId, exception)
        Log.d(TAG, "Invalid server key: $exception")
    }

    companion object {
        private const val TAG = "MyFirebaseMessagingService"
    }
}

In this code, we override the onSendError method to handle invalid server key errors. We log the error message and can take any necessary action, such as updating the server key.

Handling network errors

If there is a network error while sending a push notification, we may receive a "SendError" error. To handle network errors, we can implement the FirebaseMessagingService class and override the onSendError method. Add the following code to handle network errors:

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onSendError(messageId: String, exception: Exception) {
        super.onSendError(messageId, exception)
        Log.d(TAG, "Network error: $exception")
    }

    companion object {
        private const val TAG = "MyFirebaseMessagingService"
    }
}

In this code, we override the onSendError method to handle network errors. We log the error message and can take any necessary action, such as retrying the send operation later.

Testing Push Notifications

To ensure that push notifications are working correctly in our Android app, we need to test them on both physical devices and emulators. We can also use the Firebase Cloud Messaging console for testing.

Testing push notifications on a physical device

To test push notifications on a physical device, make sure that the device is connected to the internet and has the app installed. Use a server or script to send a push notification to the device using the device's token. The push notification should be received on the device and displayed as expected.

Testing push notifications on an emulator

To test push notifications on an emulator, make sure that the emulator is running and has the app installed. Use a server or script to send a push notification to the emulator using the emulator's token. The push notification should be received on the emulator and displayed as expected.

Using Firebase Cloud Messaging console for testing

The Firebase Cloud Messaging console allows us to send push notifications to specific devices or topics for testing purposes. To use the console for testing, go to the Firebase Console and select your project. Navigate to the "Cloud Messaging" tab and click on "New notification". Enter the necessary details, such as the target device or topic, and click on "Send test message". The push notification should be received on the selected devices or topics.

Conclusion

In this tutorial, we have learned how to implement push notifications in an Android app using Kotlin. We started by setting up the project and configuring Firebase Cloud Messaging. We then implemented push notifications by registering the device, handling token refresh, and receiving and displaying push notifications. We also explored how to customize push notifications by adding custom data, creating custom notification layouts, and handling notification actions. Finally, we discussed how to handle push notification errors and test push notifications on physical devices, emulators, and using the Firebase Cloud Messaging console. With this knowledge, you can now enhance your Android apps by adding push notifications using Kotlin.