Blog Infos



At this point we have an infrastructure to robustly show notifications. Now we are ready to declare ‘clients’ for this infrastructure. Initially we decided to support 3 type of notifications sources :

  • Remote
  • Triggered
  • Scheduled

This final chapter is unveiling implementation details for them.

Notification sources components is high-level design



Remote notifications source


Remote notifications are traditionally managed by implementation of some Service from your push notification vendor. It could be Google with Firebase Cloud Messaging or Huawei with Huawei Messaging Service or any other vendor you prefer. It could be even your own in-house built service.

As a singular entry point to notifications framework is a NotificationQueue, then we are pretty flexible to choose/switch any implementation details (like a cloud messaging vendor).

// :remote-notifications <1>
class RemoteMessagingService : FirebaseMessagingService() {

  lateinit var notificationMetadataProvider: NotificationMetadataProvider

  lateinit var notificationQueue: NotificationQueue

  lateinit var context: Context

  override fun onMessageReceived(message: RemoteMessage) {
    val remoteData =

    if (remoteData.isNotEmpty()) { // <2>
      val iconUrl = remoteData["icon"]!!
      val title = remoteData["titleText"]!!
      val description = remoteData["description"]!!
      val textColor = remoteData["fontColor"]!!
      val gradientStartColor = remoteData["bgColorFrom"]!!
      val gradientEndColor = remoteData["bgColorTo"]!!
      val action = when (remoteData["action"]) {
        "memory" -> ACTION_OPEN_MEMORY // <3>
        "battery" -> ACTION_OPEN_BATTERY
        "antihack" -> ACTION_OPEN_ANTIHACK
        else -> null

      val data = NotificationData(
        id = notificationMetadataProvider.getIdFor(this::class), // <4>
        channel = NotificationData.Channel.REMOTE,
        type = NotificationData.Type.SYSTEM,
        icon = NotificationData.Icon.Url(iconUrl),
        title = title,
        description = description,
        backgroundGradientStart = Color.parseColor(gradientStartColor),
        backgroundGradientEnd = Color.parseColor(gradientEndColor),
        textColor = Color.parseColor(textColor),
        action = action

      val loadIconRequest = ImageRequest.Builder(context) // <5>
        .target {
          runBlocking(Dispatchers.IO) { notificationQueue.add(data) }

  1. Remote notification is placed in a separate module. I’m considering it as a standalone feature of app
  2. Parsing remoteData and extracting fields for NotificationData
  3. Action for each feature is declared in a dedicated feature module. See further
  4. Getting a stable notification Id
  5. As we getting url to icon from remote, it’s need to be loaded first, and only then we able to show notification


Updated integration structure


As actions, that representing navigation to particular feature are declared in proprietary feature module, then :remote-notifications have to depend on it.

The common rule of scalability here is to prohibit features to depend on each other. Instead each feature could declare a thin API module with a very restricted amount of data. Usually it’s interfaces, constants, event buses etc.

Then the feature module declares its api module as an API configuration dependency to let its clients (which is typically :app only) to know about it.

Finally features can depend on API modules or other features.


Triggered notifications source


Triggered notifications are reactions at some events in the system. It could be:

  • New app install
  • Power Charging status changing
  • Wi-Fi connection establishment
  • any other events you can subscribe to.

Typically observers of these events are BroadcastReceivers, but in the case of your own events — it can be implemented in different ways.

So there is no special abstractions for triggers. As a before, it’s needed only NotificationQueue as a API of notifications framework.

Triggers can be either declared in a related feature module or even in a separate module if it contains a big piece of logic.



Scheduled notifications source


Notifications that are scheduled to show within some period. Also it is reasonable to apply some conditions to showing. Let’s consider a case, when in some interval each feature wants to show a notification that engages the user to interact with it. If a user interacts with a feature within this interval notification shouldn’t be shown.

// :notifications-api
interface NotificationSchedule {

 suspend fun update() // <1>

 suspend fun getLastUpdateTimestamp(): Long // <2>

 suspend fun getNotification(): NotificationData // <3>

Job Offers

Job Offers

There are currently no vacancies.


, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...


  1. API for updating a schedule
  2. We are returning last update timestamp from each schedule to have some singular logic (some Scheduler) that gathers all the timestamps and deciding which notifications should be shown
  3. Notification to be shown to engage a user

Each feature that is interested in such a way of showing logic should provide an implementation of this interface:

// :battery
internal val BATTERY_OPTIMIZED_AT_KEY = longPreferencesKey("battery_optimized_at")

class BatteryNotificationsSchedule @Inject constructor(
 private val context: Context,
 private val preferences : DataStore<Preferences>,
 private val notificationMetadataProvider: NotificationMetadataProvider
) : NotificationSchedule {

 private val notification = NotificationData(
    id = notificationMetadataProvider.getIdFor(this::class),
    channel = NotificationData.Channel.SCHEDULED,
    type = NotificationData.Type.FULLSCREEN,
    icon = NotificationData.Icon.Res(R.drawable.ic_battery),
    title = context.getString(R.string.battery_scheduled_notification_title),
    description = context.getString(R.string.notification_description),

  override suspend fun update() {
    preferences.edit {
      it[BATTERY_OPTIMIZED_AT_KEY] = System.currentTimeMillis() // <1>
    notificationQueue.remove(notification) // <2>

  override suspend fun getLastUpdateTimestamp(): Long =[BATTERY_OPTIMIZED_AT_KEY] ?: 0L

  override suspend fun getNotification(): NotificationData = notification
  1. Storing last update timestamp in SharedPreferences
  2. If the notification show was suspended (it came to the queue in the middle of the polling interval) and the schedule was updated before notification shown then we should remove notification from the queue since it’s not actual anymore.

Scheduler is a class which manages the interval of scheduling, collecting all the schedules and choosing which notification should be shown. Managing the interval can be implemented via AlarmLoopReceiver.

// :notifications
private const val ACTION_SCHEDULES_POLL = "schedule_poll"

internal class NotificationsScheduler : AlarmLoopReceiver() {

 lateinit var schedules: Set<NotificationSchedule> // <1>

 lateinit var queue: NotificationQueue

 lateinit var config: NotificationsConfig

 override lateinit var alarmManager: AlarmManager

 override val loopPeriod: Long
   get() = config.schedulePeriod

 override val loopAction: String

 override fun onReceive(context: Context, intent: Intent) {
   super.onReceive(context, intent)
   if (intent.action != ACTION_SCHEDULES_POLL) return

   runBlocking(Dispatchers.IO) {

     val notification =
         .filter { System.currentTimeMillis() - it.getLastUpdateTimestamp() > loopPeriod } // <2>
         .map { it.getNotification() }
         .randomOrNull() // <3>

     if (notification != null) {

  1. We have to provide a set of all schedules somehow. Some DI frameworks provide multibindings feature, some don’t and you have to provide this set to DI tree by hand
  2. Once within loopPeriod we are getting all schedules which update timestamp is too old
  3. And if there are few of them then choose randomly which notification should be shown

As a AlarmLoopReceiver this scheduler will robustly starts at BOOT_COMPLETED event or at app startup.


Notifications framework


Eventually, we came up with a notifications framework that focused mostly on reliability. Notifications are able to be queued, and the queue is persistent. Polling of the queue is starting at device boot or at app startup.
It’s able to show high-importance fullscreen notifications at different API levels.
It has a singular entry point that allows it to be abstracted from the implementation details of your app and just keep robustly doing its work — showing notifications.

This article was originally published on



In Chapter 1 we have chosen a Looper and Queue as a high-level design…
Showing notifications is a great mechanism for engaging users at mobile platforms. Smart management…
In Chapter 2 we provided a robust mechanism for queueing notifications. The system is…

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.