Android Push Notifications with Kotlin: A Step-by-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.