Simple Example to Use WorkManager and Notification

Simple app to explore different ways of scheduling background task using WorkManager and post a notification to the user when the task is complete

ยท

5 min read

Simple Example to Use WorkManager and Notification

This is just a very simple app that covers some basic WorkManager usage. It shows you how to schedule:

  • one-time work request

  • periodic work request

and

  • post a notification when the task is done.

For a complete guide, refer to the official WorkManager guide here.

Add WorkManager Dependency

build.gradle.kts example

dependencies {
    /*...*/
    implementation ("androidx.work:work-runtime-ktx:2.7.1")
}

Inherit CoroutineWorker Class

To run a background task, you need to create a worker class that inherits the CoroutineWorker class. The overridden doWork() method is where you perform your background tasks.

class DemoWorker(
    private val appContext: Context, 
    params: WorkerParameters)
    : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        delay(5000) //simulate background task 
        Log.d("DemoWorker", "do work done!")

        return Result.success()
    }
}

By default, the coroutine runs on Dispatchers.Default. To switch to a different coroutine dispatcher you can use CoroutineScope.withContext(). For more details, you can visit my previous blog post here.

Instantiate WorkManager

WorkManager is a Singleton. You can retrieve it by passing the application context to the WorkManager.getInstance() API.

WorkManager.getInstance(applicationContext)

You can pass in activity context too, it gets converted to application context anyway.

Once you have the WorkManager, you can set the work constraints and schedule the work request.

Set Work Constraints

You can specify work constraints for the work request. The following is an example of creating NetworkType constraints. NetworkType.CONNECTED means the work runs on when your phone is connected to the internet.

private val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()

For other work constraints, refer to the official document here.

Schedule One-time Work Request

The following examples assume you have the following variable setup.

private lateinit var workManager: WorkManager
private val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()
private val workName = "DemoWorker

This creates a one-time work request, with the NetworkType.CONNECTED constraint. To schedule the work request, you can call WorkManager.enqqueneUniqueWork().

val workRequest = OneTimeWorkRequestBuilder<DemoWorker>()
    .setConstraints(constraints)
    .build()

workManager.enqueueUniqueWork(
    workName,
    ExistingWorkPolicy.REPLACE,
    workRequest)

ExistingWorkPolicy.REPLACE enum value means if the same work exists, it cancels the existing one and replaces it.

If your work requires higher priority to run, you can call the WorkRequest.Builder.setExpedited() API. OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST means if the app is run out of expedited quota, it falls back to non-expedited/regular work requests.

val workRequest = OneTimeWorkRequestBuilder<DemoWorker>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .setConstraints(constraints)
    .build()

Schedule Periodic Work Request

val workRequest = PeriodicWorkRequestBuilder<DemoWorker>(
    repeatInterval = 15, 
    TimeUnit.MINUTES
)
    .build()

workManager.enqueueUniquePeriodicWork(
    workName,
    ExistingPeriodicWorkPolicy.REPLACE,
    workRequest)

You use PeriodicWorkRequestBuilder<>() to build a periodic work request. However, one very important note is the repeatInterval must be equal to or greater than 15 minutes. This doesn't seem to be documented anywhere.

If you specify repeatInterval less than 15 minutes, it just ignores your work request silently. Your app won't crash. There is this warning in your log, but I bet you likely won't see it. Bad decision, it should just crash the app in my opinion.

Interval duration lesser than minimum allowed value; Changed to 900000

When you call workManager.enqueueUniquePeriodicWork(), your task runs immediately and runs again at a specified repeatInterval. However, if you don't want to run the tasks immediately, you call the WorkRequest.Builder.setInitialDelay() API.

val workRequest = PeriodicWorkRequestBuilder<DemoWorker>(
    repeatInterval = 16, 
    TimeUnit.MINUTES
)
    .setInitialDelay(5, TimeUnit.SECONDS)
    .build()

The above code runs the first task after 5 seconds and repeats the task every 15 minutes.

Cancel Work Request

You can cancel the work request by passing in a unique work name parameter to the WorkManager.cancelUniqueWork() API.

workManager.cancelUniqueWork(workName)

Declare POST_NOTIFICATIONS Permission

Starting on Android 13 (API level 33 / Tiramisu), you need to declare the notification permission and request it at run time.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <!--application element-->
</manifest>

Request Runtime Permission

This is a simple runtime permission dialog composable function that launches the permission request dialog in your app.

@Composable
fun RuntimePermissionsDialog(
    permission: String,
    onPermissionGranted: () -> Unit,
    onPermissionDenied: () -> Unit,
) {

    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

        if (ContextCompat.checkSelfPermission(
                LocalContext.current,
                permission) != PackageManager.PERMISSION_GRANTED) {

            val requestLocationPermissionLauncher = 
                rememberLauncherForActivityResult(
                    ActivityResultContracts.RequestPermission()
            ) { isGranted: Boolean ->

                if (isGranted) {
                    onPermissionGranted()
                } else {
                    onPermissionDenied()
                }
            }

            SideEffect {
                requestLocationPermissionLauncher.launch(permission)
            }
        }
    }
}

To call it, you can just pass in the permission string and the callback tells you whether your permission is granted or denied.

@Composable
fun MainScreen(viewModel: MainViewModel) {
    RuntimePermissionsDialog(
        Manifest.permission.POST_NOTIFICATIONS,
        onPermissionDenied = {},
        onPermissionGranted = {},
    )
}

[Update - July 15, 2023]: To properly implement the runtime permissions, you may want to read the following blog post.

Create Notification Channel

Starting from API 26 / Android Orea (Oeatmeal Cookie), a notification channel is required if you want to post a notification.

This is an example of creating a notification channel in the DemoWorker coroutine worker class.

class DemoWorker(
    private val appContext: Context, 
    params: WorkerParameters
) : CoroutineWorker(appContext, params) {

    private val notificationChannelId = "DemoNotificationChannelId"

    /*...*/

    private fun createNotificationChannel()
    {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

            val notificationChannel = NotificationChannel(
                notificationChannelId,
                "DemoWorker",
                NotificationManager.IMPORTANCE_DEFAULT,
            )

            val notificationManager: NotificationManager? =
                getSystemService(
                    applicationContext,
                    NotificationManager::class.java)

            notificationManager?.createNotificationChannel(
                notificationChannel
            )
        }
    }
}

Create the Notification

To create the notification, you call NotificationCompat.Builder() by passing in the application context and the notificationChannelId that is used to create the notification channel in the previous step.

class DemoWorker(
    private val appContext: Context, 
    params: WorkerParameters
) : CoroutineWorker(appContext, params) {

    private val notificationChannelId = "DemoNotificationChannelId"

    /*...*/

    private fun createNotification() : Notification {
        createNotificationChannel()

        val mainActivityIntent = Intent(
            applicationContext, 
            MainActivity::class.java)

        var pendingIntentFlag by Delegates.notNull<Int>()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            pendingIntentFlag = PendingIntent.FLAG_IMMUTABLE
        } else {
            pendingIntentFlag = PendingIntent.FLAG_UPDATE_CURRENT
        }

        val mainActivityPendingIntent = PendingIntent.getActivity(
            applicationContext,
            0,
            mainActivityIntent,
            pendingIntentFlag)


        return NotificationCompat.Builder(
            applicationContext, 
            notificationChannelId
        )
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setContentTitle(applicationContext.getString(R.string.app_name))
            .setContentText("Work Request Done!")
            .setContentIntent(mainActivityPendingIntent)
            .setAutoCancel(true)
            .build()
    }

    /*...*/
}

The mainActivityPendingIntent is used to start your app's main activity when the notification is clicked.

Override getForegroundInfo()

If you use notifications in your worker class, you need to also override the getForegroundInfo() suspend function. Your app crashes without this override.

override suspend fun getForegroundInfo(): ForegroundInfo {
    return ForegroundInfo(
        0, createNotification()
    )
}

Post the Notification

To post a notification, you use NotificationManagerCompat.Notify() API.

class DemoWorker(
    private val appContext: Context, 
    params: WorkerParameters
) : CoroutineWorker(appContext, params) {

    private val notificationChannelId = "DemoNotificationChannelId"

    override suspend fun doWork(): Result {

        /* task is complete */

        if (ActivityCompat.checkSelfPermission(
                appContext,
                Manifest.permission.POST_NOTIFICATIONS) 
                    == PackageManager.PERMISSION_GRANTED
        ) {
            with(NotificationManagerCompat.from(applicationContext)) {
                notify(0, createNotification())
            }
        }

        return Result.success()
    }

    /*...*/
}

Done

I also applied a very similar code to my simple RSS feed reader app to perform background article sync every 24 hours.

This is done by this SyncWorker class.

Source Code

GitHub Repository: Demo_WorkManager

Did you find this article valuable?

Support Vincent Tsen by becoming a sponsor. Any amount is appreciated!

ย