Simple Example to Use WorkManager and Notification
Tags: android, kotlin, beginners, jetpackcompose
Published: 2023-03-03 23:00:00

**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](https://developer.android.com/topic/libraries/architecture/workmanager).

## Add WorkManager Dependency

**build.gradle.kts example**

```kotlin
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.

```kotlin
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](https://vtsen.hashnode.dev/kotlin-coroutines-basics-simple-android-app-demo#heading-coroutinescopewithcontext).

## Instantiate WorkManager

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

```kotlin
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.

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

For other work constraints, refer to the official document [here](https://developer.android.com/guide/background/persistent/getting-started/define-work#work-constraints).

## Schedule One-time Work Request

The following examples assume you have the following variable setup.

```kotlin
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().`

```kotlin
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.

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

## Schedule Periodic Work Request

```kotlin
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.

```kotlin
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.

```kotlin
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.

```xml
<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.

```kotlin
@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.

```kotlin
@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.

- [How to Request Android Runtime Permissions using Jetpack Compose?](https://vtsen.hashnode.dev/how-to-request-android-runtime-permissions-using-jetpack-compose)

## 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.

```kotlin
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.

```kotlin
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.

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

## Post the Notification

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

```kotlin
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.

- [Simple RSS Feed Reader - Jetpack Compose](https://vtsen.hashnode.dev/simple-rss-feed-reader-jetpack-compose) 

This is done by this [SyncWorker](https://github.com/vinchamp77/AndroidNews/blob/master/app/src/main/java/vtsen/hashnode/dev/androidnews/app/workers/SyncWorker.kt) class.

## Source Code

GitHub Repository: [Demo\_WorkManager](https://github.com/vinchamp77/Demo_WorkManager)

---

*Originally published at [https://vtsen.hashnode.dev](https://vtsen.hashnode.dev/simple-example-to-use-workmanager-and-notification)*.