Using Android WorkManger effectively

Mohamed Fadl Allah
HungerStation
Published in
5 min readMar 2, 2023

Introduction

This blog aims to show how we can we use Work Manager in a more effective and efficient way. WorkManager is the recommended Android API for persisting backgrounds tasks during app restarts or device reboots.

It can be useful when sending analytics data, syncing application data to server or executing any other long running task which are resumable after the app closure or device restart.

It is not recommended for background tasks that could be completed or safely terminated during app lifecycle, with no need to resume after completion.

WorkManager handles three types of persistent work; Immediate, Long Running and Deferrable.

Getting Started

  • Adding dependency to app build.gradle:
dependencies {
implementation “androidx.work:work-runtime-ktx:2.7.1”
}

You can find latest release version here 👉 WorkManager | Releases

  • Extending Worker abstract class and implement doWork with the work you need to persist.
class DownloadImageWorker(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
override fun doWork(): Result {

// Do the work here in this case.
....

// Indicate whether the work finished successfully with the Result
// could be other Result upon purpose Result.failure(), Result.retry()
return Result.success()
}
  • Build and use WorkRequest (and its subclasses) to define how and when the work should be run.
val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadImageWorker>().build()
  • Submit downloadWorkRequest object to WorkManager instance to handle it.
WorkManager.getInstance(myContext).enqueue(downloadWorkRequest)

So in the above code, the user can leave the app after a work request is enqueued and in progress. When back to the foreground, work request will be resumed without any extra effort.

Expedited work request

Within WorkManager there is another useful functionality which can expedite work request tasks which are important to the user or are user-initiated that can be complete within a few minutes like sending an email with attachment, making a payment or sending chat message.

Here is how to mark the work request to be expedited:

private val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadImageWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).build()

In the above snippet, if the app does not have any expedited job quota, the expedited work request will fall back to a regular work request like the one declared in the Introduction code snippets.

Also, if the system load is too high, when too many tasks are running, or when the system doesn’t have enough memory the expedited task will be deferred until system has enough memory to start.

If policy OutOfQuotaPolicy.DROP_WORK_REQUEST is set and if the app does not have any expedited job quota, the expedited work request will be dropped and no work requests are enqueued.

After expediting work request, contrary to regular work request, work manager will execute the work immediately when app moves to background. However the execution time is limited to quota given by the system to each app. You can learn more about it in this link 👉 https://developer.android.com/topic/performance/appstandby

Schedule periodic work requests

In some cases, you may want to schedule periodic tasks in your app which normally are not user initiated. You can run periodic work request in your app with a minimum interval of 15 mins. Also, you can add a flex time which is a time slot to start the work within the repeat interval.

  • Flex start time = repeatInterval — flexInterval
  • Flex end time = end of repeatInterval

eg: If Repeat = 60m and Flex = 15m then Flex time slot starts from 45m and ends at 60m.

val myUploadWork = PeriodicWorkRequestBuilder<DownloadImageWorker>(
1, TimeUnit.HOURS, // repeatInterval (the period cycle)
15, TimeUnit.MINUTES) // flexInterval
.build()

Work constraints

You can add constraints to periodic work requests, so when the due period is reached, the work can be suspended until all constraints are met or skipped to the next work due period.

And in case a one time work request has unmet constraints, work will be stopped by WorkManager and will be retried when all constraints are met.

Types of constraints: NetworkType, BatteryNotLow, Charging, StorageNotLow, DeviceIdle

Here is an example of how to pass work constraints to work request:

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
val myWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setConstraints(constraints)
.build()

Observing work result

  • By adding tag to your work requests so you can simply observe live data of the results or cancel them all WorkManager.cancelAllWorkByTag(WORK_TAG)
  • Or by enqueuing a single work request with a unique name WorkManager.enqueueUniqueWork(), WorkManager.enqueueUniquePeriodicWork(), you can also add a name conflict policy in case there were multiple requests running with the same name.
private val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadImageWorker>()
.addTag(WORK_TAG).build()
private val saveToStorageWorkRequest = OneTimeWorkRequestBuilder<SaveImageWorker>()
.addTag(WORK_TAG).build()
WorkManager.getInstance(this).enqueueUniqueWork("sendLogs",..

You can observe live data by tag or by unique name:

workManager.getWorkInfosByTagLiveData(WORK_TAG)
.observe(this) { workInfosList ->
....
}

Other capabilities

  • You can add initial delay for regular/periodic work requests. setInitialDelay(10, TimeUnit.MINUTES) For periodic requests delay will be applied only on first repeat interval.
  • You can return Result.retry() from your work request in case of work failure so your work could be retried. According to retry policy, retries intervals also calculated a minimum wait delay to be set before the first retry.
  • Setting data to work request:
val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadImageWorker>()
.setInputData(
workDataOf("IMAGE_URL" to "http://..")
).build()
  • Retrieving data:
override suspend fun doWork(): Result {
val imageUriInput = inputData.getString("IMAGE_URL") ?: "" return Result.failure()
  • Work manager supports long running tasks which exceed 10 mins to finish. In this case, your work must give a signal to the system to say that “keep this process alive if possible while executing.” One common use case for that is syncing an app that depends on a large offline data, so your work should override getForegroundInfo() and call setForeground(getForegroundInfo()) so work manager will shows a configurable notification and run foreground service for the work.

Chaining work requests

You can chain multiple work requests so a work request will wait for one or multiple requests to finish, then it can start running using their results.

In this example saveToStorageWorkRequest will remain in Blocked state until downloadWorkRequest is succeeded with result. If downloadWorkRequest is failed or cancelled then also saveToStorageWorkRequest will be finished by the same state.

WorkManager.getInstance(myContext)
// work requests to run in parallel
.beginWith(listOf(downloadWorkRequest))
.then(saveToStorageWorkRequest)
.enqueue()

Conclusion

In the article, I tried to walk through the main important capabilities and usages of WorkManager and to list the different ways to persist important work using it, I also added 👉 a sample application on Github that contains the most important utilization of WorkManager capabilities.

Useful links to dig more into WorkManager (debug, test, etc.)

https://developer.android.com/guide/background/persistent

https://www.kodeco.com/20689637-scheduling-tasks-with-android-workmanager

--

--