Kahibaro
Discord Login Register

21.3 WorkManager

Overview of WorkManager

WorkManager is a library from Android Jetpack that helps you run deferrable background work that must be executed reliably, even if the app is closed or the device restarts. It is designed for tasks that are important to complete, but do not need to run immediately and do not require the app to be in the foreground.

Typical examples include syncing data with a server, uploading logs, backing up files, or periodically refreshing content. WorkManager chooses the best way to run this work on each device, using JobScheduler, AlarmManager, or other mechanisms under the hood, so you do not need to worry about device version differences.

WorkManager is used for reliable, deferrable background work that must eventually run, even after app restarts, and that is not tied to the user actively using the app.

Work types in WorkManager

WorkManager organizes work using units called WorkRequests. Each WorkRequest is associated with a Worker class that contains the code you want to run.

There are three main types of work that WorkManager can handle.

The first is one time work. This is a unit of work that should run once, for example uploading a file after it is created. You usually use a OneTimeWorkRequest for this.

The second is periodic work. This is work that should run repeatedly at an interval, such as syncing data every few hours. You use a PeriodicWorkRequest for this. Periodic work has a minimum interval that Android enforces, and it is not suitable for very frequent tasks.

The third way of organizing work is through chains of work. You can create a sequence of multiple OneTimeWorkRequests that should run one after another. For example, you may compress a file first, then upload it, then clean up temporary files. WorkManager ensures that the next work runs only when the previous one finishes successfully, unless you configure it otherwise.

Creating a Worker

To use WorkManager, you define a class that extends Worker or one of its variants. This class defines what happens when the work actually runs.

A basic Worker implementation usually takes the application context and worker parameters in its constructor, and overrides the doWork function. The doWork function runs on a background thread and must return a Result that tells WorkManager whether the work succeeded, failed, or should be retried.

Here is a simple example of a Worker that uploads data.

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.Data
class UploadWorker(
    context: Context,
    workerParams: WorkerParameters
) : Worker(context, workerParams) {
    override fun doWork(): Result {
        return try {
            val inputText = inputData.getString("text_to_upload") ?: ""
            // Do the upload here, such as calling a repository or network client
            val output = Data.Builder()
                .putString("upload_status", "success")
                .build()
            Result.success(output)
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

In this example, you can see the use of inputData and outputData. This is how you pass simple data into and out of a Worker. WorkManager serializes this data so it can survive process death.

A Worker must return one of Result.success(), Result.failure(), or Result.retry() from doWork. This return value tells WorkManager how to handle the next step for this unit of work.

Enqueuing work requests

Once you have a Worker class, you need to create a WorkRequest and enqueue it with WorkManager. This is what schedules the work to run.

For one time work, you typically create a OneTimeWorkRequest using its builder, and then use WorkManager.getInstance(context).enqueue(request).

import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Data
fun scheduleUpload(context: Context, text: String) {
    val inputData = Data.Builder()
        .putString("text_to_upload", text)
        .build()
    val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
        .setInputData(inputData)
        .build()
    WorkManager.getInstance(context)
        .enqueue(uploadRequest)
}

For periodic work, you use PeriodicWorkRequestBuilder and specify the repeat interval. Keep in mind there is a platform enforced minimum interval such as 15 minutes.

import java.util.concurrent.TimeUnit
import androidx.work.PeriodicWorkRequestBuilder
fun schedulePeriodicSync(context: Context) {
    val syncRequest =
        PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
            .build()
    WorkManager.getInstance(context)
        .enqueue(syncRequest)
}

You can also chain work by using beginWith and then to define a sequence before calling enqueue.

fun scheduleChainedWork(context: Context) {
    val compressRequest = OneTimeWorkRequestBuilder<CompressWorker>().build()
    val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>().build()
    val cleanUpRequest = OneTimeWorkRequestBuilder<CleanUpWorker>().build()
    WorkManager.getInstance(context)
        .beginWith(compressRequest)
        .then(uploadRequest)
        .then(cleanUpRequest)
        .enqueue()
}

Constraints and conditions

WorkManager lets you specify conditions under which your work is allowed to run. These conditions are called constraints. Common constraints include requiring the device to be charging, requiring a network connection, or requiring the battery to not be low.

You create a Constraints object and set it on your WorkRequest builder. WorkManager will delay the execution of the work until the constraints are met.

import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
fun scheduleUploadWithConstraints(context: Context) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED)
        .setRequiresCharging(true)
        .build()
    val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
        .setConstraints(constraints)
        .build()
    WorkManager.getInstance(context)
        .enqueue(uploadRequest)
}

If you define constraints, your task will not run immediately if the conditions are not met. WorkManager monitors the device state and starts the task when it becomes valid to run.

Constraints control when work can execute. If constraints are not met, WorkManager delays work, it does not force it to run in the background under bad conditions like no network or low battery.

Unique work and replacing behavior

Sometimes you want to avoid scheduling the same work multiple times. WorkManager allows you to give work a unique name, and define what should happen if you enqueue work with that same name again.

You use methods like enqueueUniqueWork for one time work or enqueueUniquePeriodicWork for periodic work. You also pass a policy that controls whether to keep the existing work, replace it, or append new work to the existing chain.

For example, if you have a daily sync job and you never want more than one copy of it scheduled, you can do this.

import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import java.util.concurrent.TimeUnit
fun scheduleUniqueDailySync(context: Context) {
    val dailySyncRequest =
        PeriodicWorkRequestBuilder<DailySyncWorker>(24, TimeUnit.HOURS)
            .build()
    WorkManager.getInstance(context)
        .enqueueUniquePeriodicWork(
            "daily_sync",
            ExistingPeriodicWorkPolicy.KEEP,
            dailySyncRequest
        )
}

With ExistingPeriodicWorkPolicy.KEEP, if a work with the name "daily_sync" is already scheduled, WorkManager keeps the existing work and ignores the new request. Other policies like REPLACE will cancel the old work and use the new one.

Observing work status

WorkManager stores the state of each WorkRequest in a local database. This state can be observed, which lets your UI react to changes such as progress, success, or failure.

Each WorkRequest has an id. You can call getWorkInfoByIdLiveData to observe its WorkInfo. The WorkInfo contains state and optional progress or output data.

import androidx.lifecycle.Observer
import androidx.work.WorkManager
fun observeWork(context: Context, workId: java.util.UUID) {
    val workManager = WorkManager.getInstance(context)
    workManager.getWorkInfoByIdLiveData(workId)
        .observeForever { workInfo ->
            if (workInfo != null) {
                val state = workInfo.state
                if (state.isFinished) {
                    val status = workInfo.outputData.getString("upload_status")
                    // React to finished state, perhaps update UI or log
                }
            }
        }
}

In a real app that follows Android architecture components, you would usually observe this LiveData in a ViewModel and update the UI in an Activity or Fragment.

You can also cancel work using its id or its unique name. For example, WorkManager.getInstance(context).cancelWorkById(workId) stops pending and running instances of that work.

Choosing WorkManager in your app

When you design background tasks, you should think about how critical the work is, whether it needs to run right away, and whether the user should be directly aware of it. WorkManager is particularly good when work must be guaranteed to run eventually, and can wait for suitable conditions.

If your task needs to run while the user is actively interacting with your app, you would typically use other tools such as coroutines in a ViewModel. If you need a long running foreground task with a visible notification, you usually use a Service combined with a foreground notification. For periodic or deferrable tasks that must survive app restarts, WorkManager is usually the most suitable choice.

By understanding how to create Worker classes, enqueue WorkRequests, configure constraints, and observe work state, you can reliably handle many background tasks in your Android apps without managing low level scheduling details yourself.

Views: 1

Comments

Please login to add a comment.

Don't have an account? Register now!