Work Manager in Android

WorkManager is a special Android component that can execute long-running tasks even when the host Android application is in the background. With WorkManager you can use different strategies to ensure successful task completion, specify various execution constraints (e.g. execute only when connected to Wi-Fi), create tasks chains and more.

In this article, I’ll describe the high-level rationale behind WorkManager and then show you how to integrate this useful component into Android application.

WorkManager Use Cases

The overall goal of WorkManager is to ensure that important tasks get executed even when the application is in the background.

For example, if your app needs to pull data from the server periodically, WorkManager is the way to go. If you need to upload large files to your backend, WorkManager can ensure that all of them are uploaded, even if the network connectivity is lost or the device is restarted in the process. Or, maybe, your app performs CPU-intensive batch image processing? Then you can use WorkManager to run this task only when users’ device is charging to avoid draining their battery.

The above are just few examples of what you can do with WorkManager, and a complete list of use cases would probably take a full blog post on its own. Truly, WorkManager is very useful and versatile component.

Legacy Alternatives to WorkManager

Over the years, there had been no shortage of different solutions to execute background tasks in Android apps. Starting with the official components like AlarmManager, SyncAdapter, FirebaseJobDispatcher, JobScheduler and more, and all the way to third-party solutions like Priority Job Queue and Android Job. It was a crazy wild west of background work back then, and the recommended approach changed every 1-2 years.

Luckily for us, WorkManager either replaced or deprecated all the legacy solutions. You can still find them in older projects, but they shouldn’t be used in a new code.

WorkManager Gradle Dependencies

Let’s switch gears and learn how to integrate WorkManager into your Android application.

First, you need to declare the corresponding dependency in your module’s build.gradle file:

    // Java
    implementation "androidx.work:work-runtime:$version"
    // Kotlin + Coroutines
    implementation "androidx.work:work-runtime-ktx:$version"

You can find a list of the availble versions here.

Implementing a Worker

The most fundamental building block in WorkManager framework is an abstract class called ListenableWorker. It represents a single asynchronous task that WorkManager can execute. However, for most use cases, it’s simpler to extend ListenableWorker‘s abstract subclass called Worker, which exposes a synchronous doWork() method that will be called on a background thread.

This is an example of a custom Worker:

class MyWorker(
    context: Context,
    workerParams: WorkerParameters,
): Worker(context, workerParams) {

    override fun doWork(): Result {
        Log.i("MyWorker", "work started")
        Thread.sleep(2000L) // simulate some work
        Log.i("MyWorker", "work finished")
        Result.success()
    }
}

The return value from doWork() method is one of the subclasses of Result abstract class:

  • Success: work completed successfully.
  • Retry: work didn’t complete successfully and should be retried.
  • Failure: work didn’t complete successfully and should not be retried.

WorkManager will process the result returned from doWork(), account for additional configuration parameters, and then decide what to do next. If the task succeeds, WorkManager will proceed to execute other tasks in a chain that were blocked by the completed task. If the task should be retried, WorkManager will schedule another attempt according to its backoff configuration. If the task fails, WorkManager will automatically cancel all other tasks in a chain that depend on the failed task.

It’s important to remember that if WorkManager retries a task, it creates a completely new instance of MyWorker class and calls doWork() method of that new instance. Therefore, any custom in-memory state (class properties) that you might’ve initialized in the previous instance of MyWorker will not be carried over to the next attempt.

Starting a Worker

The above MyWorker class defines a task that WorkManager can potentially execute. To actually start this task, you’ll need to enqueue a new WorkRequest:

val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,
        30,
        TimeUnit.SECONDS
    ).build()

WorkManager.getInstance(context).enqueue(workRequest)

Just for the sake of demonstration, I configured the above OneTimeWorkRequest with a custom backoff strategy for potential retries.

Please note that WorkRequest has two subclasses: OneTimeWorkRequest and PeriodicWorkRequest. The later should be used if you want WorkManager to execute the scheduled task periodically.

Adding Work Constraints

As I mentioned earlier, WorkManager allows you to specify various constraints that must be satisfied before it’ll execute a specific task.

For example, that’s how you add a constraint for MyWorker to be started only when the device is connected to the network:

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

val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
    .setConstraints(constraints)
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,
        30,
        TimeUnit.SECONDS
    ).build()

WorkManager.getInstance(context).enqueue(workRequest)

You can see the full list of constraints supported by WorkManager here.

Passing Custom Runtime Arguments into Worker

In some cases, you might want to pass custom runtime arguments into your Worker. To do that, you’ll need to instantiate Data object (which is similar to Bundle) and then bind it to the respective WorkRequest:

val inputData = Data.Builder()
    .putInt("retries", 3)
    .build()

val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
    .setInputData(inputData)
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,
        30,
        TimeUnit.SECONDS
    ).build()

WorkManager.getInstance(context).enqueue(workRequest)

Then you can retrieve the “input data” from within the Worker started by the corresponding WorkRequest.

For example, this is an enhanced version of MyWorker which simulates a task that needs to be retried several times before it succeeds:

class MyWorker(
    context: Context,
    workerParams: WorkerParameters,
): Worker(context, workerParams) {

    override fun doWork(): Result {
        val retries = inputData.getInt("retries", 1)
        Log.i("MyWorker", "work started")
        Thread.sleep(2000L) // simulate some work
        Log.i("MyWorker", "work finished")
        if (runAttemptCount < retries) {
            Result.retry()
        } else {
            Result.success()
        }
    }
}

Passing Custom Constructor Arguments into Worker

The previous implementations of MyWorker class had two constructor arguments, but we didn’t handle these dependencies ourselves. That’s because these are the “default” dependencies that WorkManager can provide automatically. However, in most real-world scenarios, you’ll probably want to pass additional custom dependencies into Worker‘s constructor. That’s especially true if you strive to write a clean code and employ Dependency Injection architectural pattern.

For example, consider this implementation of MyWorker that requires NotificationManager as an additional constructor argument (we’ll make use of this dependency later in this article):

class MyWorker(
    context: Context,
    workerParams: WorkerParameters,
    private val notificationManager: NotificationManager,
): Worker(context, workerParams) {

    override fun doWork(): Result {
        val retries = inputData.getInt("retries", 1)
        Log.i("MyWorker", "work started")
        Thread.sleep(2000L) // simulate some work
        Log.i("MyWorker", "work finished")
        if (runAttemptCount < retries) {
            Result.retry()
        } else {
            Result.success()
        }
    }
}

If you attempt to enqueue the same WorkRequest as before after this change, WorkManager will fail. The reason is simple: WorkManager has no clue where to get an instance of NotificationManager from. So, it becomes your responsibility to provide this dependency.

To support custom constructor arguments in Workers, we’ll need to implement our own WorkerFactory:

class MyWorkerFactory constructor(
    private val notificationManager: NotificationManager,
): WorkerFactory() {

    override fun createWorker(
        appContext: Context,
        workerClassName: String,
        workerParameters: WorkerParameters
    ): ListenableWorker {
        if(workerClassName == MyWorker::class.java.name) {
            return MyWorker(appContext, workerParameters, notificationManager)
        } else {
            throw RuntimeException("unsupported worker class: $workerClassName")
        }
    }
}

Then we’ll need to instruct WorkManager to use this new factory instead of the default one.

Unfortunately, WorkManager uses the ContentProvider hack for auto-initialization, so we’ll need to disable it first. Add these lines into your AndroidManifest.xml:

 <provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <!-- If you are using androidx.startup to initialize other components -->
    <meta-data
        android:name="androidx.work.WorkManagerInitializer"
        android:value="androidx.startup"
        tools:node="remove" />
 </provider>

After disabling the auto-initialization, we can initialize WorkManager manually inside our custom subclass of Application and configure it to use our new WorkerFactory (note that MyApplication implements Configuration.Provider interface):

class MyApplication: Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration {
        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        return Configuration.Builder()
            .setWorkerFactory(MyWorkerFactory(notificationManager))
            .build()
    }
}

At this point, after we injected our custom WorkerFactory into WorkManager’s configuration, we’ll be able to enqueue instances of MyWorker that depend on NotificationManager.

Starting Expedited Work

WorkManager’s primary goal is executing background tasks which aren’t as latency-sensitive as foreground operations in most cases. Therefore, WorkManager can defer tasks’ execution to optimize the overall device performance. However, in some situations, you might need to execute a task as quickly as possible, while also leveraging other WorkManager’s features. Enter expedited work requests.

When you define a work request as expedited, WorkManager will do its best to execute it right away This is very handy and convenient for individual applications, but prevents WorkManager from employing most of its optimization strategies which, in turn, can negatively affect the overall user experience. Therefore, the authors of WorkManager put limits on expedited work to make sure that developers don’t abuse this mechanism.

To start an expedited work, just add one line of code when you construct your WorkRequest:

val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
    .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
    .setBackoffCriteria(
        BackoffPolicy.LINEAR,
        30,
        TimeUnit.SECONDS
    ).build()

WorkManager.getInstance(context).enqueue(workRequest)

The argument of setExpedited() method instructs WorkManager what to do when your app runs out of its quota for expedited work. In the above example, WorkManager will simply treat that WorkRequest as non-expedited once quota limit is reached.

There is one big caveat when it comes to expedited work, though: a “native” support for expedited work requests was added only in Android 12. To make this mechanism backwards-compatible, WorkManager will start a Foreground Service when it executes expedited work on Android 11 and earlier.

As you might know, Foreground Service requires a notification to be shown to the user, so, if you want to use expedited work, your Workers must also override getForegroundInfo() method (that’s where NotificationManager instance that we injected into MyWorker previously will be used):

class MyWorker(
    context: Context,
    workerParams: WorkerParameters,
    private val notificationManager: NotificationManager,
): Worker(context, workerParams) {

    override fun doWork(): Result {
        val retries = inputData.getInt("retries", 1)
        Log.i("MyWorker", "work started")
        Thread.sleep(2000L) // simulate some work
        Log.i("MyWorker", "work finished")
        if (runAttemptCount < retries) {
            Result.retry()
        } else {
            Result.success()
        }
    }

    override fun getForegroundInfo(): ForegroundInfo {

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

        val pendingIntent: PendingIntent = PendingIntent.getActivity(
            applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )

        createNotificationChannel()

        val notification: Notification = Notification.Builder(applicationContext, CHANNEL_ID)
            .setContentTitle("notification title")
            .setContentText("notification text")
            .setSmallIcon(R.drawable.ic_logo)
            .setContentIntent(pendingIntent)
            .build()

        return ForegroundInfo(NOTIFICATION_ID, notification)
    }

    private fun createNotificationChannel() {
        val channel = NotificationChannel(
            CHANNEL_ID,
            "channel name",
            NotificationManager.IMPORTANCE_DEFAULT
        )
        channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
        notificationManager.createNotificationChannel(channel)
    }
}

If you don’t return a valid ForegroundInfo from getForegroundInfo() method, then your expedited work will not be executed on devices powered by Android 11 or earlier.

Summary

In this post we discussed the main use cases for WorkManager and you learned how to use this handy framework in your Android application.

WorkManager is very powerful and versatile component. Unfortunately, this component is also quite complex and has a large API surface. In my opinion, most of this complexity is totally justified by the advanced features that WorkManager implements. Furthermore, since WorkManager replaced a bunch of legacy approaches, overall, it reduced the complexity of background work in Android apps considerably.

Thanks for reading and please leave your comment and questions below.

Check out my premium

Android Development Courses

7 comments on "Work Manager in Android"

  1. Another great blog post, thank you very much! After reading this article, I revisited my worker and successfully tried out the approach of creating a custom WorkerFactory. However, in my case, almost every worker requires different arguments. The solution for this is called “DelegatingWorkerFactory.” It allows you to add multiple WorkerFactories. Another important step is to return “null” from a custom WorkerFactory if the argument “workerClassName” is not found within the factory. In that case, the default WorkerFactory is used, which tries to find the worker by reflection and then inserts the default arguments “Context” and “WorkerParameters.”

    Reply
    • Hi Robert,
      Thanks for providing additional information. As far as I know, the main use case for DelegatingWorkerFactory is to allow third-party libs provide their own Workers. In the scope of your own application, I don’t see why you wouldn’t be able to accommodate different Worker constructor arguments using a single factory. Do I miss something?
      As for returning “null”, this is a great tip. I’ll edit the post!

      Reply
  2. Hi,
    For anyone getting into workmanager, this is extremely informative. However, it would make it better if you briefly went over the PeriodicWorkRequest, as for most scheduler-like tasks that is very necessary given current restrictions on background executions.

    Reply
  3. Good Article .

    So in Android 12 and above the Foreground notification won’t be displayed?
    dowork() will execute without showing to user that something being done in background?
    In Android 12 and above i see work being executed without showing a foreground notification.
    While 11 and below it shows the same.

    Reply

Leave a Comment