1. Home
  2. Android
  3. Android WorkManager Tutorial: Getting Started

Android WorkManager Tutorial: Getting Started

Priya Sindkar

Priya Sindkar

Native Android Developer. Avid Reader.

Last updated on

Table of Contents

Android WorkManager is a background processing library which is used to execute background tasks which should run in a guaranteed way but not necessarily immediately. With WorkManager we can enqueue our background processing even when the app is not running and the device is rebooted for some reason. WorkManager also lets us define constraints necessary to run the task e.g. network availability before starting the background task.

Android WorkManager is a part of Android Jetpack (a suite of libraries to guide developers to write quality apps) and is one of the Android Architecture Components (collection of components that help developers design robust, testable, and easily maintainable apps).

Illust2
Exploring the Android Workmanager

When to use WorkManager while working with background processing?

With the evolution of Android OS over the years, there are restrictions placed on background processing, in order to optimize battery consumption and make use of device resources in an optimal way. Each new Android release, starting from android marshmallow (API 23), has added some restrictions. You can read about the specific details on the same in the Android developer guide.

Thus, it is important to choose the best background processing approach for your app per your needs. Let’s quickly talk about cases when you would choose to use WorkManager.

Android WorkManager can be a perfect background processing library to use when your task –

1. Does not need to run at a specific time

2. Can be deferred to be executed

3. Is guaranteed to run even after the app is killed or device is restarted

4. Has to meet constraints like battery supply or network availability before execution

The simplest example for this can be when your app needs to upload a large chunk of user data to the server. This particular use case meets the criteria we mentioned above to choose WorkManager because:

1. Results need not be reflected immediately in your Android app

2. Data needs to be uploaded even when the upload begins and the user kills the app to work on some other app, and

3. The network needs to be available in order to upload data on the server.

This blog by Pietro Maggi, Android Developer Advocate @Google gives a detailed explanation about when and how to use WorkManager.

Creating a background work with Android WorkManager

To get started with implementing WorkManager, we first need to create a work which defines the task we need to run in the background using WorkManager. For brevity, we will consider uploading user data to server as our use case throughout this article. To define work, we will create a class which extends the Worker class. The task to be performed in background is written in the overridden method doWork().

The code below shows an example of how to write a Worker class.

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters

class UserDataUploadWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
    override fun doWork(): Result {
        uploadUserData()
        return Result.success()
    }

    private fun uploadUserData() {
        // do upload work here
    }
}

Working with WorkRequests

After we define our work, we now need to define how and when we should start the work. One of the advantages of using WorkManager is that we can define whether our work is a one-time task or the work needs to be called periodically.

OneTimeWorkRequest

OneTimeWorkRequest is a concrete implementation of the WorkRequest class which is used to run WorkManager tasks that are to be executed once. The code below shows how to create OneTimeWorkRequest in its simplest form:

val uploadWorkRequest = OneTimeWorkRequestBuilder<UserDataUploadWorker>().build()

In order to run this work request, we need to call enqueue() method on an instance of WorkManager and pass this WorkRequest as shown below:

WorkManager.getInstance().enqueue(uploadWorkRequest)

The enqueue() method enqueues one or more WorkRequests to be run in the background.

PeriodicWorkRequest

PeriodicWorkRequest, as the name suggests is used to run tasks that need to be called periodically until cancelled.

A few important points to keep in mind when working with PeriodicWorkRequest:

1. The work is run multiple times

2. The work keeps on executing periodically until it is cancelled

3. The first execution happens immediately after the mentioned constraints are met and the next execution occurs after the mentioned period of time

4. Recurring execution doesn’t begin if the constraints mentioned with that work request are not met

5. The minimum repeat interval is 15 minutes

6. The work cannot be chained with other work requests

7. The time interval between 2 periodic intervals can differ based on OS optimization restrictions we saw earlier

A PeriodicWorkRequest can be executed as shown below:

val periodicWorkRequest = PeriodicWorkRequestBuilder<UserDataUploadWorker>(24, TimeUnit.HOURS).build()
WorkManager.getInstance().enqueue(periodicWorkRequest)

The code above creates a periodic work request which is executed every 24 hours.

In order to stop the execution of periodic work, it needs to be explicitly cancelled:

WorkManager.getInstance().cancelWorkById(periodicWorkRequest.id)

Constraints in Android WorkManager

WorkRequests can also be built with some constraints that need to be satisfied in order to execute the task. Constraints in WorkManager are specifications about the requirements that must be met before the work request is executed. For our use case, if we need to upload user data on our server, having network availability is a must. Thus, through the work request constraints, we can make sure that our upload task is executed only when the network is available as shown below:

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

val oneTimeWorkRequest = OneTimeWorkRequestBuilder<UserDataUploadWorker>()
            .setConstraints(uploadDataConstraints)
            .build()

There are many other constraints available with Android WorkManager including ‘requires charging’, ‘storage is not low’, ‘device is not idle’, etc. A list of all these constraints can be found below:

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

One work request can have multiple constraints defined on it. In case of multiple constraints, the work is executed only when all the constraints are met. If any constraint is not satisfied, the work is stopped and resumed only when that constraint is met.

Input Data, Worker Results, and Output Data in WorkManager

Input Data

Running your background task often needs some data to work on. Like in our example, if we want to send user data to the server via a Worker, we need to pass that user data to the Worker task. In Android WorkManager, we can do so by using the Data class. The input data is nothing but a list of key-value pairs. The data can be passed to the WorkRequest as shown below:

import androidx.work.*

....

val userDataJson = "{some data...}"
val someOtherData = false

// using workDataOf() method from KTX
val inputWorkData = workDataOf("user_data" to userDataJson, "other_data" to someOtherData)

// using Data.Builder
val inputData = Data.Builder()
     .putString("user_data", userDataJson)
     .putBoolean("other_data", someOtherData)
     .build()val oneTimeWorkRequest = OneTimeWorkRequestBuilder<UserDataUploadWorker>()
    .setConstraints(uploadDataConstraints)
    .setInputData(inputData)
    .build()

As seen in the code above, the data can be constructed in 2 ways:

1. By using workDataOf() method which is part of KTX and which converts list of pairs as Data

2. By using the Data.Builder class which constructs a map from the passed key-value pairs and builds Data from this map

In order to access this input data inside the Worker, getInputData() is called inside the doWork() method to extract individual values from their keys like this:

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters

class UserDataUploadWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
    override fun doWork(): Result {
        // fetch the input data just like you would fetch values from a map
        val userDataJson = inputData.getString("user_data")
        val someOtherData= inputData.getBoolean("other_data", false) // pass false as default value
        uploadUserData()
        return Result.success()
    }

    private fun uploadUserData() {
        // do upload work here
    }
}

Worker Results and Output Data

Result class is used to return the status of our worker task. doWork() method of the Worker class which is overridden to do the background tasks, expects an object of class Result as its return type. Work Result can be of three types:

1. Result.success() – This result indicates that the work was completed successfully

2. Result.failure() – This result indicates that the work was permanently terminated and that if there is any chain of workers following this worker, all the following workers will also be terminated. So we should return Result.failure() only when our chain of workers is dependent on this worker’s output

3. Result.retry() – This result indicates that the work is terminated for some reason and that it should be retried/ re-executed

Great, so what if we also want to pass some output data back from the worker once our background processing is over?

The methods, success() and failure() of the Result class take in Data as an argument. This Data is same as the one used for Input Data and can constructed in the same manner, with a list of key-value pairs.

An example of passing output data from Worker is shown below:

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf

class UserDataUploadWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
    override fun doWork(): Result {
        val isSuccess = uploadUserData()
        val outputData = workDataOf("is_success" to isSuccess)

        return Result.success(outputData)
    }

    private fun uploadUserData(): Boolean {
        // do upload work here
        return true
    }
}

But how do you access this output data from the Worker?

If you follow android developer’s official documentation, you might try to extract the output by using WorkInfo like this:

val workInfo = WorkManager.getInstance().getWorkInfoById(oneTimeWorkRequest.id).get()
val wasSuccess = workInfo.outputData.getBoolean("is_success", false)
Toast.makeText(this, "Was work successful?- $wasSuccess", Toast.LENGTH_LONG).show()

If you execute the code above, you will notice that the output data is never passed from the worker and the method getBoolean() on outputData will just resort to returning the default value i.e. false. We can double check if we have indeed passed output data from the worker but the behaviour still remains the same.

The reason behind not getting any output data is that the Worker executes all the tasks in background synchronously. When we attempt to fetch the output data from the WorkInfo, we try to fetch the output data before the worker is even executed. You can check this by running getStatus() method on the WorkInfo instance.

WorkManager.getInstance().getWorkInfoById(oneTimeWorkRequest.id).get().state

Calling the state immediately after enqueuing the WorkRequest will give the state as ENQUEUED and hence don’t get any output data.

To get the output data, we need to observe the WorkInfo via LiveData so that our app will attempt to retrieve the output data only when the worker is finished with its execution.

Observing Worker state via LiveData in WorkManager

Android WorkManager lets you observe the states of the Worker by returning LiveData for the requested WorkInfo and observing this LiveData for any changes. Thus, now to access the output data, we need to write an observer on the WorkInfo LiveData, check if its state is SUCCEEDED, and then extract the output data. Check the code below:

 WorkManager.getInstance()
            .getWorkInfoByIdLiveData(oneTimeWorkRequest.id).observe(this, Observer { workInfo ->
                val wasSuccess = if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
                    workInfo.outputData.getBoolean("is_success", false)
                } else {
                    false
                }

              Toast.makeText(this, "Work was success: $wasSuccess", Toast.LENGTH_LONG).show()
})

Closing Thoughts

To summarise, we looked at:

  1. What Android WorkManager is
  2. When exactly to use WorkManager (for background processing)
  3. How to execute it and applying constraints
  4. Passing data to and from the WorkManager workers
  5. Observing WorkManager task for output data

With these basics in mind, I hope you will now be able to implement deferrable background tasks on modern-day android devices running upgraded android OS after lollipop.

// Find jobs by category

You've got the vision, we help you create the best squad. Pick from our highly skilled lineup of the best independent engineers in the world.

Copyright @2024 Flexiple Inc