A smart way to build and display notification for Android
Imo Okon
Android Developer
September 9
An Android app I worked on is personal app, and it is using push notifications capability. Push notifications are a nice way to keep your users engaged, and from the business perspective, taking the advantages, they provide is crucial.
Displaying notifications on Android is fairly easy. The documentation is very nice and clean, and describes the best practices and requirements to make the UX compliant to the standards. In today’s world, the standard way to send a push to a device is by using the Firebase. If there are no customizations or custom pay load, the effort on the Android side in order to display a notification, branded specifically for the app is matter of defining some properties (like icon etc) in xml. However, this post is about receiving push notifications with custom payload, and display different notification based on that payload. The way I solve this I find pretty handy and I think it deserves to be shared, so other people may find it useful too, or they may get inspired to write a better one.
Let’s make a step to code studio for coding
First, we have to define the dependencies for google play services and Firebase. It’s very well explained here what are the dependencies and their latest versions, so we will skip that step and start with defining services in our AndroidManifest.xml file:
<service
android:name=”.notifications.InstanceIdService”
android:exported=”false”>
<intent-filter>
<action android:name=”com.google.firebase.INSTANCE_ID_EVENT” />
</intent-filter>
</service>
<service
android:name=”.notifications.MessagingService”
android:permission=”signature”>
<intent-filter>
<action android:name=”com.google.firebase.MESSAGING_EVENT” />
</intent-filter>
</service>
The first one is the one in charge of receiving the events when Firebase is refreshing the device token, and the second one is about receiving the push notifications coming from Firebase. When Firebase refreshes the token, we have to send it to our backend so it gets updated and the device will keep receiving pushes. The implementation is rather pretty straight forward:
class InstanceIdService : FirebaseInstanceIdService()
{
@Inject internal lateinit var presenter: PushNotificationsTokenPresenter
override fun onCreate() {
AndroidInjection.inject(this)
super.onCreate()
}
override fun onTokenRefresh() {
val token = FirebaseInstanceId.getInstance().token
token?.let {
presenter.performTokenRefresh(it)
}
}
}
And the second service is the one that receives the push notifications. All it does is just deliver the received data into the PushNotification class that knows how to display a notification. Here is how it looks like:
class MessagingService : FirebaseMessagingService() {
@Inject internal lateinit var pushNotification: PushNotification
override fun onCreate() {
AndroidInjection.inject(this)
super.onCreate()
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
pushNotification.show(applicationContext, remoteMessage.data)
}
}
The interesting part
It’s important to mention that the data that is received in the remote message is a Map, and one of key-value pairs inside is an id. This id is very important, because by using that id we will distinguish different types of push notifications, and ultimately we will display different notification. Let’s take a look at the implementation of the PushNotification class:
@Singleton
internal class PushNotification @Inject constructor(private val notificationManager: NotificationManager,
private val resolver: NotificationItemResolver,
private val notificationBuilder: NotificationBuilder) {
fun show(context: Context, data: Map<String, String>) {
val id = data[“id”]?.hashCode() ?: 0
val notification = resolver.resolve(context, data)
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) {
createChannel(context, notification.channel())
}
notificationManager.notify(id, notificationBuilder.build(context, notification))
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannel(context: Context, channel: PushNotificationChannel) {
val channelTitle = context.getString(channel.titleResource)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val notificationChannel = NotificationChannel(channel.channelId, channelTitle, importance)
notificationChannel.setShowBadge(true)
notificationChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
notificationManager.createNotificationChannel(notificationChannel)
}
}
To start of, let’s get through the class dependencies:
· NotificationManager is the Android’s SDK notification manager that is used for showing the notifications by it’s notify() method.
· NotificationItemResolver is an interface that provides a contract for resolving the received data into a type that will be used to build a displayable notification
· NotificationBuilder is an interface that provides a contract for making android.app.Notification out of the type that was resolved by the resolver.
And here is how they are constructed
@Provides
@Singleton
fun providePushNotificationManager(context: Context): NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Provides
@Singleton
fun providePushNotificationItemResolver(): NotificationItemResolver =
PushNotificationItemResolver()
@Provides
@Singleton
fun provideNotificationBuilder(): NotificationBuilder =
DefaultNotificationBuilder()
Since the NotificationManager is pretty straight forward, let’s take a look at the NotificationItemResolver and it’s concrete implementation:
internal interface NotificationItemResolver {
fun resolve(context: Context, data: Map<String, String>): PushNotificationItem
}
internal class PushNotificationItemResolver : NotificationItemResolver {
override fun resolve(context: Context, data: Map<String, String>): PushNotificationItem {
val content = data[“value”] ?: “”
return when (data[“id”]) {
“friendSuggestion” -> FriendSuggestionNotification(context, content)
else -> PushNotificationItem.Empty()
}
}
}
The implementation of the interface is basically a factory that produces different types of PushNotificationItem based on the id value that comes from Firebase that we discussed before. When we would like to add new type, we will have to add new implementation of PushNotificationItem (which we will check in a moment) and define the mapping in the resolve() method. The PushNotificationItem is an interface that defines the basics of the notification that will be displayed:
internal interface PushNotificationItem {
fun channel(): PushNotificationChannel
fun title(): String
fun message(): String
fun smallIcon(): Int
fun pendingIntent(): PendingIntent
}
When targeting O, the API requires us to provide a channel in order to display a notification. The channel is sort of a group, and the user could enable/disable different channels. Therefore, the channel has an id and a title, and this is what it looks like:
internal sealed class PushNotificationChannel(val channelId: String, @StringRes val titleResource: Int) {
class Friends : PushNotificationChannel(“friends”, R.string.friendsNotifications)
class Empty : PushNotificationChannel(“empty”, R.string.genericNotifications)
}
Next, the PushNotificationItem interface defines title, message, smallIcon, and pendingIntent. When the notification will be displayed, this are the items that are going to be displayed. A notification can have many other different things for content, but for this example we take only the basics. Also, a very important thing is the PendingIntent which describes what is going to be opened in the app upon click on the notification.
So far, we have defined two PushNotificationItem types that are resolved in our PushNotificationItemResolver implementation, mainly the PushNotificationItem.Empty, and FriendSuggestionNotification. Let’s get through the second one:
internal class FriendSuggestionNotification(private val context: Context,
private val content: String) : PushNotificationItem {
override fun channel() = PushNotificationChannel.Friends()
override fun smallIcon(): Int = R.drawable.ic_notification
override fun title(): String = context.getString(R.string.friendSuggestionNotificationTitle)
override fun message(): String = context.getString(R.string.friendSuggestionNotificationMessage)
override fun pendingIntent(): PendingIntent {
val resultIntent = Intent(context, FriendsActivity::class.java)
resultIntent.putExtra(FriendsActivity.FRIEND_SUGGESTION, content)
resultIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
val requestID = System.currentTimeMillis().toInt()
return PendingIntent.getActivity(context, requestID, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
The empty one is quite same, just with different values correspondingly. And it just creates pending intent that opens the app’s main screen.
So far we’ve seen the mechanism behind mapping and preparing the notification data based on the received data from Firebase. We’ve seen that adding new type is as easy as defining a new class that derives from the PushNotificationItem and defining it in the resolver. If it has to go to a new channel that doesn’t exist, we will also have to define new channel class that derives from PushNotificationChannel and that’s it.
Now, let’s take a look at the definition and the implementation of the NotificationBuilder that actually creates a notification that would be actually displayed:
internal interface NotificationBuilder {
fun build(context: Context, item: PushNotificationItem): Notification
}
internal class DefaultNotificationBuilder : NotificationBuilder {
override fun build(context: Context, item: PushNotificationItem): Notification {
val builder = NotificationCompat.Builder(context, item.channel().channelId)
.setSmallIcon(item.smallIcon())
.setContentTitle(item.title())
.setContentText(item.message())
.setAutoCancel(true)
.setDefaults(Notification.DEFAULT_ALL)
.setContentIntent(item.pendingIntent())
return builder.build()
}
}
As we may see, the implementation here is fairly simple. It just maps the values from the PushNotificationItem into a android.app.Notificationusing the NotificationCompat.Builder
Getting back to the show() method of the PushNotification class where we started off, it does the things pretty straight forward: first maps the data into a PushNotificationItem, and if the current build version is O, it also creates a channel. Then it triggers a notify() on the manager with the android.app.Notification that got built out of the PushNotificationItem
As we’ve seen so far, this solution is quite well separated and easy to deal with. It’s easy to provide translation for the notification texts, as all that has to be done is defining translated strings.xml without a need to do any changes on the notifications themselves. Defining a new type of notification requires the backend to send different id for that type, and a new class for it on the Android side. Changing the way the data is mapped, or extending the functionality is as easy as replacing the implementations of the interfaces. And ultimately, and very importantly — this solution is testable. It’s easy to write unit tests for the way the NotificationItemResolver does resolve the data and if it does properly, as well as writing tests for the actual PushNotificationItem types, and check if they return the desired values.
I hope somebody will find this useful and handy, and of course, I’m ready to get into some more explanations as maybe required from me.