1. Overview
This codelab extends the Jetsnack sample app with a simulated checkout experience that uses notifications to track an order.
The developer will migrate the app from using standard notifications
To using the ProgressStyle template.
In addition, they will apply the necessary changes to promote these notifications as Live Updates; bringing more visibility and adding a richer user experience to the tracking of the order.
Prerequisites
- Understanding of how to create Notifications
What you'll learn
- How ProgressStyle notifications enhance progress centric experiences.
- How Live updates bring more visibility to user-initiated progress centric experiences.
What you'll need
- A working computer and reliable wifi
- A GitHub account
- Android Studio with Android 16 Platform and SDK Level 36 downloaded (Android 16).
2. Checking out the code
Check out the code. You can download the starter code or the completed code from GitHub.
Starter code
$ git clone git@github.com:android/codelab-live-updates.git -b starter_code
$ cd codelab-live-updates
Completed code
$ git clone git@github.com:android/codelab-live-updates.git
$ cd codelab-live-updates
3. The checkout experience
We will now run the app and go through the simulated checkout experience.
- Open Android Studio and open the
codelab-live-updates
project that was checked out. - Either through a flashed device running Android 16 or through the Android Studio emulator running Android 16, run the app.
- Go to the shopping cart icon in the app to view your preloaded cart.
- Grant the app the notification permission via the "Grant" button at the bottom.
- Click on checkout to simulate the checkout process.
| ||
|
|
At this point you will be notified of your order status through a series of notifications.
4. Implementation overview
The core class responsible for the notifications is in SnackbarNotificationManager.kt. In here you'll see an OrderState enum that captures the various states your simulated order goes through and the notification that is built for that specific stage of your order.
private enum class OrderState(val delay: Long) {
INITIALIZING(5000) {
...
},
FOOD_PREPARATION(12000) {
...
},
FOOD_ENROUTE(18000) {
},
...
The start() method is where a Handler is used to schedule the posting and updating of the notification, going through those various states.
fun start() {
for (state in OrderState.entries) {
val notif = state.buildNotification().build()
Handler(Looper.getMainLooper()).postDelayed({
notificationManager.notify(NOTIFICATION_ID, notif)
}, state.delay)
}
}
The app currently uses standard/non-styled notifications to keep the user informed of their order status. Let's now migrate the app to using ProgressStyle to enhance the user experience of tracking an order.
5. ProgressStyle notification migration
ProgressStyle is a notification style that leverages a highly customizable progress bar for tracking.
We will start by updating our compile SDK version to 36 (Android 16).
In libs.versions.toml, update the compileSdk to 36
compileSdk = "36"
Next, we will go to SnackbarNotificationManager.kt and create the base ProgressStyle notification that we will customize for each OrderState.
Base ProgressStyle notification
Copy the code below into the SnackbarManagerNotification class OrderState enum.
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
fun buildBaseProgressStyle(orderState : OrderState): ProgressStyle {
val pointColor = Color(236,183, 255, 1).toArgb()
val segmentColor = Color(134,247,250,1).toArgb()
val progressStyle = ProgressStyle()
.setProgressPoints(
listOf(
ProgressStyle.Point(25).setColor(pointColor),
ProgressStyle.Point(50).setColor(pointColor),
ProgressStyle.Point(75).setColor(pointColor),
)
).setProgressSegments(
listOf(
ProgressStyle.Segment(25).setColor(segmentColor),
ProgressStyle.Segment(25).setColor(segmentColor),
ProgressStyle.Segment(25).setColor(segmentColor),
ProgressStyle.Segment(25).setColor(segmentColor)
)
)
when (orderState) {
INITIALIZING -> {}
FOOD_PREPARATION -> {}
FOOD_ENROUTE -> progressStyle.setProgressPoints(
listOf(
ProgressStyle.Point(25).setColor(pointColor)
)
)
FOOD_ARRIVING -> progressStyle.setProgressPoints(
listOf(
ProgressStyle.Point(25).setColor(pointColor),
ProgressStyle.Point(50).setColor(pointColor)
)
)
ORDER_COMPLETE -> progressStyle.setProgressPoints(
listOf(
ProgressStyle.Point(25).setColor(pointColor),
ProgressStyle.Point(50).setColor(pointColor),
ProgressStyle.Point(75).setColor(pointColor)
)
)
}
return progressStyle
}
Resolve all the unknown objects by importing the necessary classes.
import android.app.Notification.ProgressStyle
import android.os.Build
In the buildBaseProgressStyle method we define a progress style notification with Notification.ProgressStyle.ProgressPoints and Notification.ProgressStyle.ProgressSegments to capture the milestones and transitions of the order respectively.
The order has 4 progress points: FOOD_PREPARATION, FOOD_ENROUTE, FOOD_ARRIVING, and ORDER_COMPLETE and are positioned in the progress bar in increments of 25, each increment conveying the progression of the order until its completion at progress value 100.
There are also 4 segments between those milestones, capturing those transitions.
Note that we colored these points and segments to match the branding of the app.
When building the base ProgressStyle notification, we pass in the order state to dynamically change the colorization of the progress style points in the when statement. This is done to visually represent past order states with colorization that matches the branding of the app.
Note, that these steps aren't necessary with the usage of ProgressStyle.Builder#setProgress(int) to update the progress bar. We only do this to apply custom colorization vs using the default colorization. See Notification.ProgressStyle#setStyledByProgress(boolean) for details.
In the next step, we'll update the notifications to use ProgressStyle and in each order state and build on top of this base ProgressStyle at each corresponding progress point numerical progressions to capture an even richer visual experience for conveying order status.
Changing from standard or non-styled to ProgressStyle notification
We will now modify the order states where we build our standard notifications and update them to use ProgressStyle.
In the same file, SnackManagerNotification.kt, we will modify the notifications created in the various order states to use ProgressStyle via the Notification.Builder#setStyle(android.app.Notification.Style) method. Replace the existing OrderState enum with the following code:
private enum class OrderState(val delay: Long) {
INITIALIZING(0) {
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
override fun buildNotification(): Notification.Builder {
return buildBaseNotification(appContext, INITIALIZING)
.setContentTitle("You order is being placed")
.setContentText("Confirming with bakery...")
.setStyle(buildBaseProgressStyle(INITIALIZING).setProgressIndeterminate(true))
}
},
FOOD_PREPARATION(7000) {
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
override fun buildNotification(): Notification.Builder {
return buildBaseNotification(appContext, FOOD_PREPARATION)
.setContentTitle("Your order is being prepared")
.setContentText("Next step will be delivery")
.setLargeIcon(
IconCompat.createWithResource(
appContext, R.drawable.cupcake).toIcon(appContext))
.setStyle(buildBaseProgressStyle(FOOD_PREPARATION).setProgress(25))
}
},
FOOD_ENROUTE(13000) {
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
override fun buildNotification(): Notification.Builder {
return buildBaseNotification(appContext, FOOD_ENROUTE)
.setContentTitle("Your order is on its way")
.setContentText("Enroute to destination")
.setStyle(buildBaseProgressStyle(FOOD_ENROUTE)
.setProgressTrackerIcon(IconCompat.createWithResource(
appContext, R.drawable.shopping_bag).toIcon(appContext))
.setProgress(50)
)
.setLargeIcon(
IconCompat.createWithResource(
appContext, R.drawable.cupcake).toIcon(appContext))
}
},
FOOD_ARRIVING(18000) {
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
override fun buildNotification(): Notification.Builder {
return buildBaseNotification(appContext, FOOD_ARRIVING)
.setContentTitle("Your order is arriving and has been dropped off")
.setContentText("Enjoy & don't forget to refrigerate any perishable items.")
.setStyle(buildBaseProgressStyle(FOOD_ARRIVING)
.setProgressTrackerIcon(IconCompat.createWithResource(
appContext, R.drawable.delivery_truck).toIcon(appContext))
.setProgress(75)
)
.setLargeIcon(
IconCompat.createWithResource(
appContext, R.drawable.cupcake).toIcon(appContext))
}
},
ORDER_COMPLETE(21000) {
@RequiresApi(Build.VERSION_CODES.BAKLAVA)
override fun buildNotification(): Notification.Builder {
return buildBaseNotification(appContext, ORDER_COMPLETE)
.setContentTitle("Your order is complete.")
.setContentText("Thank you for using JetSnack for your snacking needs.")
.setStyle(buildBaseProgressStyle(ORDER_COMPLETE)
.setProgressTrackerIcon(IconCompat.createWithResource(
appContext, R.drawable.check_circle).toIcon(appContext))
.setProgress(100)
)
.setLargeIcon(IconCompat.createWithResource(
appContext, R.drawable.cupcake).toIcon(appContext))
}
};
In each order state we now set the style of the notification and also build on top of that base ProgressStyle to reflect the status and progress of the order. The progress of the order is updated using the Notification.ProgressStyle.Builder#setProgress(int) method and we update the progress tracker icon using the Notification.ProgressStyle.Builder#setProgressTrackerIcon(android.graphics.drawable.Icon) to add visual cues to the end user.
The migration to use ProgressStyle is complete. Build and run the app and observe the richness of a stylized template with visualizations to capture the progress of your order.
|
| |
|
| |
|
This is a huge improvement! However we are also going to set the specific criteria for notifications that include live updates. Creating a live update notification will further improve the usability of your app as it will show live tracking of orders.
6. Live update treatment
Live updates are specific notifications that receive more visibility. They appear in various UI surfaces, are ranked higher in the notification drawer and appear as a customizable chip in the status bar.
In order for notifications to be considered a live update, they MUST MEET the following criteria:
- Must have a title. Can set with Notification.Builder#setContentTitle.
- Must be ongoing. Can set with Notification.Builder#setOngoing.
- Must use a valid style. Can use ProgressStyle, CallStyle, BigTexStyle, or no style at all.
- Must request promotion. New API: Notification.Builder#setRequestPromotedOngoing.
- App must request
android.permission.POST_PROMOTED_NOTIFICATIONS
in the AndroidManifest.
Additionally, a Notification must steer clear of the following RESTRICTIONS to become a live update:
- Must NOT be a group summary (e.g. Notification.Builder#setGroupSummary).
- Must NOT requested to be colorized (e.g. Notification.Builder#setColorized).
- Must NOT use custom views.
- Must NOT be posted to a notification channel that is minimized (i.e. IMPORTANCE_MIN).
So first, be sure to declare the permissions in your manifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--Permission for live update eligibility-->
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" />
<!--Permission for posting notifications-->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
This is a normal permission, which means it's granted to your app on install, but it is still possible for the user to revoke this permission. So limit your live updates to things that are really important to the user, and the user will allow your app to keep using them.
Back in the SnackbarManagerNotification.kt
class, we modify the buildBaseNotification method and set the required criteria. The content title and the ongoing requirement has already been set so we only add the extra, setting the key "android.requestPromotedOngoing"
to true
on the notification to tell the system that we would like to apply the live update treatment to it.
fun buildBaseNotification(appContext: Context, orderState: OrderState): Notification.Builder {
val promotedExtras = Bundle()
promotedExtras.putBoolean("android.requestPromotedOngoing", true)
val notificationBuilder = Notification.Builder(appContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
.addExtras(promotedExtras)
return notificationBuilder
}
For developers using NotificationCompat, your app can request promotion with a single method.
fun buildBaseNotification(appContext: Context, orderState: OrderState): NotificationCompat.Builder {
val notificationBuilder = NotificationCompat.Builder(appContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
.setRequestPromotedOngoing(true)
return notificationBuilder
}
Run the application with these changes; the notification has higher ranking, and now appears on the always-on lock screen. There's now also a status chip for the notification in the status bar.
Customizing the Status Chip
The status chip appears when the app is closed and by default just shows just the small icon from the notification.
The status chip is an important experience of live updates as it allows quick access to the notification and allows the developer to convey useful text regarding the ongoing activity. Currently, we have not modified the right fields that allow us to convey useful time progression to the user about their order. The chip can show content of two types:
- Plain text via Notification.Builder#setShortCriticalText
- A countdown via Notification.Builder#setWhen
Let's update our app to take advantage of this customization by adding the additional when statement in the buildBaseNotification method. For most states we will provide a status string, but in the FOOD_ENROUTE and FOOD_ARRIVING stages the app will simulate the estimated time of arrival for the order.
when (orderState) {
INITIALIZING -> notificationBuilder.setShortCriticalText("Placing")
FOOD_PREPARATION -> notificationBuilder.setShortCriticalText("Prepping")
FOOD_ENROUTE -> notificationBuilder
.setWhen(System.currentTimeMillis().plus(11 * 60 * 1000 /* 10 min */))
FOOD_ARRIVING -> notificationBuilder
.setWhen(System.currentTimeMillis().plus(6 * 60 * 1000 /* 5 min */))
ORDER_COMPLETE -> notificationBuilder.setShortCriticalText("Arrived")
}
When you run the app with these changes, the status chip will now reflect the status or arrival time of the order. The setWhen method triggers a countdown that doesn't require the developer to continuously update, however, when updating a notification, it must be set and updated to reflect the current progression of things. See Status Chips in our developer documentation for more details.
7. Programmatically understanding promotion criteria (Optional)
There are methods that allow you to understand whether your app and notifications are eligible to present live updates.
- NotificationManager#canPostPromotedNotifications() – This tells you when the device supports live updates, your app has requested the permission, and the user allows you to post live updates.
- Notification.hasPromotableCharacteristics() – This tells you that your notification is correctly constructed to be shown as a live update.
These methods can be useful for tracking how often your users are seeing the live updates on their device, or if you need your notifications to look different on devices that don't support live updates yet.
In our app, we will use these two methods to just log to the console for debugging.
fun start() {
for (state in OrderState.entries) {
val notif = state.buildNotification().build()
Handler(Looper.getMainLooper()).postDelayed({
notificationManager.notify(NOTIFICATION_ID, notif)
Logger.getLogger("canPostPromotedNotifications")
.log(
Level.INFO,
notificationManager.canPostPromotedNotifications().toString())
Logger.getLogger("hasPromotableCharacteristics")
.log(
Level.INFO,
notif.hasPromotableCharacteristics().toString())
}, state.delay)
}
}
8. Conclusion
Live updates truly enhance the way user-initiated ongoing notifications are seen. For additional resources on Live updates please visit the public documentation at goo.gle/live-updates.