As discussed in my first blog post on the subject, notifications are a great way to increase user engagement and retention over time. I built a free NotificationsDemo app for Android to help illustrate some of the concepts discussed in these blogs. The complete Github project can be found here, and you can download the app from the Google Play Store here.

Notifications in Android Nougat

For apps that might be firing multiple notifications per day, one strategy can be to use the Notification Bundle. Notifications will automatically be bundled if the same app sends 4 or more notifications that have not specified a grouping. Google says notification bundling should be used only if the following conditions are true:

  • The child notifications are complete notifications and can be displayed individually without the need for a group summary.
  • There is a benefit to surfacing the child notifications individually. For example:
    • They are actionable, with actions specific to each child.
    • There is more information to the child that the user wants to read.

Notifications Bundle example: android notifications1 Notifications Bundle collapsed: android notifications2-3 On Android 6.0 Marshmallow: android notifications4-5 When the notification is expanded on Marshmallow, it will show the inbox style summary text. When it is collapsed, it will show the content text for the group summary notification. To bundle notifications, we must have a notification that will be used as the summary notification for the group. Create a group key for this bundle and have this and all child notifications use the .setGroup() method with the same key. Designate this notification as the summary for its group by setting .setGroupSummary(true).

Notification notification0 = new NotificationCompat.Builder(this)
       .setGroup(KEY_NOTIFICATION_GROUP)
       .setGroupSummary(true)
       .setContentIntent(pendingIntent)
       .setAutoCancel(true)
       .setSmallIcon(android.R.drawable.ic_dialog_email)
       .setLargeIcon(BitmapFactory.decodeResource(getResources(),
               android.R.drawable.ic_dialog_email))
       .setContentTitle("Bundled Notifications Content Title")
       .setContentText("Content Text for group summary")
       .setStyle(new NotificationCompat.InboxStyle()
               .setSummaryText("This is my inbox style summary."))
       .setColor(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimary))
       .setLights(ContextCompat.getColor(
               getApplicationContext(), R.color.willowtree_text), 1000, 1000)
       .setVibrate(new long[]{800, 800, 800, 800})
       .setDefaults(Notification.DEFAULT_SOUND)
       .build();

To add more notifications to this group, call .setGroup() using the same group key as the summary notification.

Notification notification1 = new NotificationCompat.Builder(this)
       .setGroup(KEY_NOTIFICATION_GROUP)
       .setSmallIcon(android.R.drawable.ic_dialog_email)
       .setColor(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimaryDark))
       .setContentTitle("Toor Wolliw")
       .setContentText("!ppa na Desaeler")
       .build();

Remember to fire your notifications using the NotificationManager which can be accessed using

NotificationManager notificationManager =
     (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

notificationManager.notify(0, notification0);
notificationManager.notify(1, notification1);

App makers can also take advantage of Android Nougat’s Direct Reply feature: android notifications6 On Android 6.0 Marshmallow: android notifications7 First, create a PendingIntent that will call an Intent that will do something with the user’s input.

Intent serviceIntent = new Intent(this, ReplyService.class);
PendingIntent servicePendingIntent = PendingIntent.getService(
       this, 0, serviceIntent, PendingIntent.FLAG_ONE_SHOT);

If you will be targeting devices lower than Nougat, also create a PendingIntent and result Intent that will implement your feature without using Direct Reply, such as by starting an Activity from the action button.

/*************************************************************
* We must manually cancel a notification when it is
* opened from an action.
*************************************************************/
Intent activityIntent = new Intent(this, ReplyActivity.class);
activityIntent.putExtra(ReplyActivity.KEY_NOTIFICATION_ID, ID_DIRECT_REPLY_NOTIFICATION);
PendingIntent activityPendingIntent = PendingIntent.getActivity(
       this, 0, activityIntent, PendingIntent.FLAG_ONE_SHOT);

Next create a RemoteInput object with a key that will be used to retrieve the user input as a Bundle later on. Also set a label which will be displayed in the Direct Reply view in the notification.

RemoteInput remoteInput = new RemoteInput.Builder(KEY_USERS_REPLY)
       .setLabel(getString(R.string.reply_label))
       .build();

Next we need to set up an Action with the icon, title, and PendingIntent we want fired and then add our RemoteInput object to it.

NotificationCompat.Action remoteInputAction = new NotificationCompat.Action.Builder(
       android.R.drawable.ic_dialog_email, getString(R.string.reply_label),
       isNougat() ? servicePendingIntent : activityPendingIntent)
       .addRemoteInput(remoteInput)
       .build();

Now we can create a notification, and call .addAction() with our Action.

Notification directReplyNotification = new NotificationCompat.Builder(this)
       .setAutoCancel(true)
       .setContentIntent(activityPendingIntent)
       .setSmallIcon(android.R.drawable.ic_dialog_email)
       .setContentTitle("My Direct Response Notification")
       .setContentText("Daniel: Would you like to send a message?")
       .addAction(remoteInputAction)
       .setColor(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimary))
       .setLights(ContextCompat.getColor(
               getApplicationContext(), R.color.colorPrimary), 1000, 1000)
       .setVibrate(new long[]{800, 800, 800, 800})
       .setDefaults(Notification.DEFAULT_SOUND)
       .build();

Now notify.

notificationManager.notify(ID_DIRECT_REPLY_NOTIFICATION, directReplyNotification);

To use the input in the service, use getResultsFromIntent() in onHandleIntent(). You can then update or dismiss your notification using the same ID that was previously used.

@Override
protected void onHandleIntent(Intent intent) {
   Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
   if (remoteInput != null) {
       String response = remoteInput.getString(MainActivity.KEY_USERS_REPLY);
       //Send the user response to the server
   }

   //Update (or dismiss) the notification
   NotificationManager notificationManager =
           (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

   Notification notification = new NotificationCompat.Builder(this)
           .setSmallIcon(android.R.drawable.ic_notification_overlay)
           .setContentText(getString(R.string.response_sent))
           .setColor(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimary))
           .build();

   notificationManager.notify(MainActivity.ID_DIRECT_REPLY_NOTIFICATION, notification);
}

Other Notification styles

As we saw in the Notifications Bundle for Marshmallow and below, notifications can show different content when expanded or collapsed.

BigTextStyle Notification: android notifications8-9 The BigTextStyle notification is useful for showing a large body of text. The big text content is shown when the notification is expanded, and the properties you set in the style will be shown. When the notification is collapsed, the properties of the notification such as .setContentTitle() and .setContentText() will be shown.

private void bigTextStyleNotification() {
   final int ID = 100;
   NotificationManager notificationManager =
           (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

   Notification notification = new NotificationCompat.Builder(this)
           .setAutoCancel(true)
           .setSmallIcon(android.R.drawable.ic_dialog_email)
           .setStyle(new NotificationCompat.BigTextStyle()
                   .setBigContentTitle("BigTextContentTitle")
                   .setSummaryText("Top Summary Line")
                   .bigText("BigText: This style is useful if you need to display a large " +
                           "body of text.  When this notification is collapsed (when " +
                           "there are also other notifications in the shade or if it is " +
                           "manually collapsed), it will " +
                           "only show this notification's .setContentTitle() and " +
                           ".setContentText()."))
           .setContentTitle("BigTextStyle ContentTitle")
           .setContentText("BigTextStyle ContentText")
           .setColor(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimary))
           .setLights(ContextCompat.getColor(
                   getApplicationContext(), R.color.colorPrimary), 1000, 1000)
           .setVibrate(new long[]{800, 800, 800, 800})
           .setDefaults(Notification.DEFAULT_SOUND)
           .build();

   notificationManager.notify(ID, notification);
}

BigPictureStyle Notification: android notifications10-11 The BigPictureStyle notification is useful for showing a large picture. This is the style used when taking a screenshot.

private void bigPictureStyleNotification() {
   final int ID = 200;
   NotificationManager notificationManager =
           (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

   Notification notification = new NotificationCompat.Builder(this)
           .setAutoCancel(true)
           .setSmallIcon(android.R.drawable.ic_menu_gallery)
           .setStyle(new NotificationCompat.BigPictureStyle()
                   .setBigContentTitle("BigPictureContentTitle")
                   .setSummaryText("BigPictureSummaryText")
                   .bigLargeIcon(BitmapFactory.decodeResource(getResources(),
                           android.R.mipmap.sym_def_app_icon))
                   .bigPicture(BitmapFactory.decodeResource(getResources(),
                           android.R.mipmap.sym_def_app_icon)))
           .setLargeIcon(BitmapFactory.decodeResource(getResources(),
                   android.R.drawable.ic_menu_gallery))
           .setContentTitle("BigPictureStyle ContentTitle")
           .setContentText("BigPictureStyle ContentText")
           .setColor(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimary))
           .setLights(ContextCompat.getColor(
                   getApplicationContext(), R.color.colorPrimary), 1000, 1000)
           .setVibrate(new long[]{800, 800, 800, 800})
           .setDefaults(Notification.DEFAULT_SOUND)
           .build();

   notificationManager.notify(ID, notification);
}

Sometimes an app needs to download a large amount of data in the background. Android supplies us with a determinate as well as an indeterminate Progress Bar notification.

Determinate Progress Bar Notification: android notifications12 A good way to implement a Progress Bar Notification is to use a Foreground Service by calling startForeground() and passing the id of the notification along with the notification object itself. The notification passed to startForeground() will live as long as the service is running, but will dismiss when the service is killed. This means we can update our Progress Bar notification with progress, but to show the user that the service has finished we need to display a notification using a different ID than the one that was passed to startForeground(). To supply progress to a notification, call setProgress() passing the maximum value, incremental value, and whether or not it is indeterminate.

public class DeterminateProgressBarService extends IntentService {

   private static final String SERVICE_NAME = "determinate_service";

   public DeterminateProgressBarService() {
       super(SERVICE_NAME);
   }

   @Override
   protected void onHandleIntent(@Nullable Intent intent) {
       final int ID_DETERMINATE_SERVICE = 9001;
       final int ID_NOTIFICATION_COMPLETE = 300;
       final int MAX_PROGRESS = 250;

       NotificationManager notificationManager =
               (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

       NotificationCompat.Builder notification = new NotificationCompat.Builder(this)
               .setSmallIcon(android.R.drawable.stat_sys_download)
               .setContentTitle("ProgressBarTitle")
               .setContentText("ProgressBarText")
               .setColor(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimary));

       int progress = 0;
       notification.setProgress(MAX_PROGRESS, progress, false);
       startForeground(ID_DETERMINATE_SERVICE, notification.build());

       do {
           try {
               Thread.sleep(15); //Do some work.
               progress++;
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           notification.setProgress(MAX_PROGRESS, progress, false);
           notificationManager.notify(ID_DETERMINATE_SERVICE, notification.build());
       } while (progress < MAX_PROGRESS);

       notification.setSmallIcon(android.R.drawable.stat_sys_download_done)
               .setProgress(0, 0, false)
               .setLights(ContextCompat.getColor(
                       getApplicationContext(), R.color.colorPrimary), 1000, 1000)
               .setVibrate(new long[]{800, 800, 800, 800})
               .setDefaults(Notification.DEFAULT_SOUND);
       notificationManager.notify(ID_NOTIFICATION_COMPLETE, notification.build());

       stopSelf();
   }
}

Indeterminate Progress Bar: android notifications13 To create an Indeterminate Progress Bar Notification, pass “true” when calling setProgress() on the notification instead.

public class IndeterminateProgressBarService extends IntentService {

   private static final String SERVICE_NAME = "indeterminate_service";

   public IndeterminateProgressBarService() {
       super(SERVICE_NAME);
   }

   @Override
   protected void onHandleIntent(Intent intent) {
       final int ID_INDETERMINATE_SERVICE = 9000;
       final int ID_NOTIFICATION_COMPLETE = 400;
       final int MAX_PROGRESS = 250;

       NotificationManager notificationManager =
               (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

       NotificationCompat.Builder notification = new NotificationCompat.Builder(this)
               .setSmallIcon(android.R.drawable.stat_sys_download)
               .setContentTitle("ProgressBarTitle")
               .setContentText("ProgressBarText")
               .setColor(ContextCompat.getColor(getApplicationContext(), R.color.colorPrimary));

       int progress = 0;
       notification.setProgress(0, 0, true);
       startForeground(ID_INDETERMINATE_SERVICE, notification.build());

       do {
           try {
               Thread.sleep(15); //Do some work.
               progress++;
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       } while (progress < MAX_PROGRESS);

       notification.setSmallIcon(android.R.drawable.stat_sys_download_done)
               .setProgress(0, 0, false)
               .setContentText("Indeterminate ProgressBar finished.")
               .setLights(ContextCompat.getColor(
                       getApplicationContext(), R.color.colorPrimary), 1000, 1000)
               .setVibrate(new long[]{800, 800, 800, 800})
               .setDefaults(Notification.DEFAULT_SOUND);

       /*************************************************************
        * Our startForeground() notification will be removed when we
        * call stopSelf(), so we supply a new ID to create a notification
        * we want persisted.
        *************************************************************/
       notificationManager.notify(ID_NOTIFICATION_COMPLETE, notification.build());

       stopSelf();
   }
}

Notifications are a powerful way to increase user engagement and retention, but any notification pushed out should be meaningful or they can do more harm than good. Google has provided app makers with new types of notifications with Android Nougat, but they have also provided users with quick ways to block annoying notifications in order to protect their OS. Product designers should be careful to make sure that their notifications bring value to their users and that they won’t be received as spam.