DEV Community

Cover image for Simple Example to Use WorkManager and Notification
Vincent Tsen
Vincent Tsen

Posted on • Edited on • Originally published at vtsen.hashnode.dev

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

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")
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Schedule Periodic Work Request

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

workManager.enqueueUniquePeriodicWork(
    workName,
    ExistingPeriodicWorkPolicy.REPLACE,
    workRequest)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 = {},
    )
}
Enter fullscreen mode Exit fullscreen mode

[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
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
    }

    /*...*/
}
Enter fullscreen mode Exit fullscreen mode

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()
    )
}
Enter fullscreen mode Exit fullscreen mode

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()
    }

    /*...*/
}
Enter fullscreen mode Exit fullscreen mode

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


Originally published at https://vtsen.hashnode.dev.

Top comments (0)