Custom Notifications for Android
Why custom notifications?
Arriving at Hootsuite, my first project was to revamp our Android push notifications — from single lines of text to ones that contained the same information as our in-app notifications. This would allow customers to reply to mentions, check out new followers, publish Instagram posts and much more right from their home screen, reducing the friction of their workflow. The original plan was to switch from InboxStyle to BigPictureStyle or BigTextStyle, but it soon became clear that default styles were not enough. They forced us to choose between showing media or text when we wanted to display images with multiline text, formatted to be consistent with our in-app user interface. This article will cover creating completely custom rich notifications with actions using the DecoratedCustomViewStyle introduced in API level 24.
The original notifications, using InboxStyle. Each one was a line of text in a single Android notification, and users had to enter the app to get more value out of them:
The first iteration using BigTextStyle and BigPictureStyle. We have a lot more information and actions, but it’s a tradeoff between text and media, and we can’t format or style the Views:
The final iteration using DecoratedCustomViewStyle. The Views are completely custom and support rich media, including images, videos and GIFs:
So let’s dive into the world of rich notifications! We’ll be making a sample app that can send this notification:
You can find the complete code for it here.
Starting with the basics
We’ll use NotificationCompat.Builder, the builder class that makes it easy to specify the appearance and behavior of our NotificationCompat object. (NotificationCompat includes the features introduced to the Notification class after API level 4 while remaining backwards compatible with devices that are at a lower API level.) Custom notifications start out like any other notification:
All notifications require these three fields to be set, although you won’t actually see them once you add your custom views. We’ll add two more lines:
This ensures that the notification is automatically cancelled and MainActivity launches when the user taps it.
Now let’s make the layout files for our custom views, which we’ll eventually inflate into RemoteViews objects. (RemoteViews is a view hierarchy that can be displayed outside of your app’s process, and the only View subclass that NotificationCompat.Builder will accept as an argument for CustomContentView or CustomContentBigView.) You can style your XML layout files however you want, but keep in mind that RemoteViews only support FrameLayout, LinearLayout, RelativeLayout, and GridLayout, and a limited set of View subclasses. If you want to customize both the collapsed and the expanded view, you’ll need to create a layout file for each of them.
Here’s the layout for our collapsed notification…
and our expanded one.
Side note on styles
My sample app’s layouts look similar to the default Android notifications. If you want your UI to be consistent with the Android design as well, notifications, like any other widget, have built-in default styles that you can apply. For example, I used
to style the timestamp.
Adding the information
Notifications are most interesting when they contain relevant, dynamic information. In the sample app, we’ll make the notification display whatever the user types and also set the time stamp to be the current time. We’ll create new RemoteViews objects using our XML files.
There are numerous setter methods that you can call to programmatically populate the view — just pass in the viewId and whatever you want to put there. The ones I ended up using were setTextViewText(), setImageViewResource() and setOnClickPendingIntent(), since together they provide the features of a standard Android notification.
Adding the actions
For the demo app, we’ll just make the button trigger a Toast message helpfully telling you which button you clicked. We’ll set a PendingIntent for the button using setOnClickPendingIntent(), which allows a foreign application, such as the home screen, to execute code with your application’s permissions. We’ll make an IntentService and call PendingIntent.getService() to retrieve a PendingIntent that starts the service, since all we want to do is display a Toast.
You’ll have to set the action string whenever you have multiple actions, so that you can do a switch statement in onHandleIntent. Since the IntentService runs on a background thread that doesn’t display UI, we’ll have to make a Handler object on the main thread to make the Toast show up.
In the actual Hootsuite app, we made the Toast on the main thread using RxJava:
But that’s another story.
Don’t forget to register your IntentService in AndroidManifest.xml!
By the way, the other options for creating a PendingIntent are getActivity() (the PendingIntent begins an Activity), getActivities() and getBroadcast() (the PendingIntent performs a broadcast, and could be better choices depending on what your intended actions are.
We’re ready to turn the NotificationCompat.Builder into a *real* NotificationCompat — just call builder.build().
You’ll have to get the system level NotificationManager from the system service.
Finally, we’ll call notificationManager.notify() to send the notification to the system. We’re passing in 0 as the id parameter but if you want your app to display multiple notifications, you’ll need to pass in a unique int each time.
Here’s our complete MainActivity.java:
Final note on sizing
Sizing will vary from device to device, so it’s important to make sure our layouts will fit on every screen. The height measurements are 64 dp for a collapsed notification and 256dp for an expanded. I ended up adding maxLine attributes to TextViews and maxHeight attributes to ImageViews to prevent elements from getting pushed off the screen.
There were two main problems that rich notifications were meant to address: our old notifications didn’t contain enough information to be very useful to users, and Instagram publishing had a complicated workflow. We were happy to see that Instagram publishing notifications were the most used, with the “publish” action making up 85% of all user notification actions.
However, one customer sent feedback that he didn’t like the update because he received hundreds of notifications every day. This was fine when he’d have a single InboxStyle notification informing him that he had a few hundred unviewed messages, but when they arrived individually, they became distracting. We realized that actions are ideal for users who have enabled the few notifications that they find really important. For those with many notifications enabled, grouping their summaries into a single notification would be more useful. So now, with a simple check of the notification count, we’re sending the first four notifications as rich ones for users to action on and combining them once again into an InboxStyle notification when the count reaches five. After users clear it, the count resets and they’ll receive rich notifications again. After this change, the number of notifications cleared dropped significantly, to a tenth of what it was before. We’ll continue to listen to customer feedback and tweak notifications to improve the experience as much as possible.