πŸͺœ WorkManager πŸͺœ

A WorkManager is used to run long, periodic, or battery intensive tasks. Unlike previous APIs, you are guaranteed that your job will be executed, even if the app is closed, or the phone restarted.

implementation "androidx.work:work-runtime-ktx:2.7.1"

Access the WorkManager

val workManager = WorkManager.getInstance(this)
// or, use static methods
WorkManager.someStaticMethod()

Create a worker

class XXXWorker(c: Context, args: WorkerParameters) : Worker(c, args) {
    // use 'applicationContext' to get back the context
    override fun doWork(): Result {
        // use Result.failure()/Result.retry() on error
        return Result.success()
    }
}

To create an async worker, you can use a CoroutineWorker.

class XXXWorker(c: Context, args: WorkerParameters) : CoroutineWorker(c, args) {
    override suspend fun doWork(): Result {
        delay(5000) // example of async call
        return Result.success()
    }
}

Create a WorkRequest

If you want a job to only be performed once, you can use a OneTimeWorkRequest.

val request = OneTimeWorkRequest.from(XXXWorker::class.java)
// using the builder
val request = OneTimeWorkRequestBuilder<XXXWorker>().build()

See also PeriodicWorkRequest below.

Run a WorkRequest

A work manager is taking a WorKRequest and running it.

// process a request
workManager.enqueue(request)
// chain requests
workManager.beginWith(request)
    .then(request).then(request)/*...*/.build()

Additional notes

Add constraints on the WorkRequest

Here is an example of every constraint you can use

val constraints = Constraints.Builder()
    .setRequiresCharging(true)
    .setRequiresBatteryNotLow(true)
    .setRequiresStorageNotLow(true)
    .setRequiresDeviceIdle(true)
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .build()

Then, you can use setConstraints to pass constraints. For instance, for a OneTimeWorkRequest, you will have

val request = OneTimeWorkRequestBuilder<XXXWorker>()
+    .setConstraints(constraints)
    .build()
Add a tag to a work request

This can be used to find work requests by tag.

// βœ… good practice (in a companion object...)
private const val TAG = "SOME_TAG"
val request = OneTimeWorkRequestBuilder<XXXWorker>()
+    .addTag(TAG)
    .build()
Pass data to a worker, between workers, or return a result

The data passed is a dictionary.

// βœ… good practice (in a companion object...)
private const val KEY = "key"
private const val KEY2 = "key2"
// ➑️ Using Data.Builder
val someData = Data.Builder()
    .putString(KEY, "value")
    .putInt(KEY2, 5000)
    .build()
// ➑️ Using workDataOf
val someData = workDataOf(KEY to "value", KEY2 to 5000)

To pass data to the first task, use

val request = OneTimeWorkRequestBuilder<XXXWorker>()
+    .setInputData(someData)
    .build()

Inside a worker, to get data, use

inputData.getString(key)
...

To pass data to the following task if any, or to any observer

Result.success(someData)
Observe a worker
private val _work : LiveData<List<WorkInfo>>

_work = workManager.getWorkInfosForUniqueWorkLiveData(WID)
_work = workManager.getWorkInfoByIdLiveData(uuid)
_work = workManager.getWorkInfosByTagLiveData(TAG)

The LiveData contains a list of WorkInfo, one per worker.

To make things easier, we use Transformations, and work will only be non-null when the first task (it[0]) is completed.

// only one job, no need for a list to be public
val work: LiveData<WorkInfo>

// NOTE: this must be called after
// _work = ...
// as _work must have been initialized
work = Transformations.map(_work) {
    // not yet
    if (it.isNullOrEmpty()) {
        return@map null
    }
    // ensure that the job if finished
    return@map if (it[0].state.isFinished) it[0] else null
}

Then, do as usual

viewModel.work.observe(viewLifecycleOwner) {
    // do something when the job has finished.
    // If you have passed data, you can use it.outputData
}
Cancel work
workManager.cancelAllWork()
workManager.cancelUniqueWork(WID)
workManager.cancelWorkById(uuid)
workManager.cancelAllWorkByTag(TAG)
Unique WorkRequests

If you want to ensure there is up to one WorkRequest running at a time, you can use unique work chains.

// βœ… good practice (in a companion object...)
private const val WID = "SOME_ID"
// process a request
-workManager.enqueue(request)
+workManager.enqueueUniqueWork(WID, policy, request)
// chain requests
-workManager.beginWith(request)
+workManager.beginUniqueWork(WID, policy, request)
    .then(request).then(request)/*...*/.build()

The policy is one of these

  • ExistingWorkPolicy.REPLACE: cancel previous job then start
  • ExistingWorkPolicy.KEEP: only start if there is no pending job
  • ExistingWorkPolicy.APPEND: process after the previous unique work chain is finished
PeriodicWorkRequest

You can use PeriodicWorkRequest for requests that should be executed every X minutes, X being greater than 15.

// every 15 hours
val request = PeriodicWorkRequestBuilder<XXXWorker>(15, TimeUnit.HOURS) .build()
// and you need to use
workManager.enqueueUniquePeriodicWork(WID, ExistingPeriodicWorkPolicy.REPLACE, request)