• WorkManager Basics
  • Lyla Fujiwara
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Rickon
  • Proofread by: Feximin

WorkManager Basics

Illustration by Virginia Poltrack

Welcome to the second article in our WorkManager series. WorkManager is an Android Jetpack library for running delayable, guaranteed background work when constraints on work are met. WorkManager is the current best practice for many types of back-end work. In the first post, we discussed what WorkManager is and when to use it.

In this blog post, I will introduce:

  • Define your background tasks as work
  • Define how specific work should work
  • Run your work
  • Use chains to work with dependencies
  • Monitor the status of your work

I’ll also explain what goes on behind the scenes with WorkManager so you can make informed decisions about how to use it.

Let’s start with an example

Let’s say you have an image-editing app that lets you filter images and upload them to the web for the world to see. You want to create a set of background tasks for filtering, compressing images, and later uploading. At each stage, there are constraints that need to be checked — enough power to filter the image, enough storage space to compress the image, and an Internet connection to upload the image.

This example is shown above

This example is a task with the following characteristics:

  • Delayable, because you don’t need it to execute immediately, and you might actually want to wait for some constraint to be satisfied (for example, waiting for a network connection).
  • Make sure it works, whether the application exits or not, because your users will be very unhappy if the filtered image is never shared with the world!

These features make our image filter and upload tasks perfect use cases for WorkManager.

Add WorkManager dependencies

This article uses Kotlin to write the code, using the KTX library (Kotlin eXtensions). The KTX version of the library provides extension functions for more concise and customary use of Kotlin. You can add the following dependencies to use the KTX version of WorkManager:

dependencies {
 def work_version = "1.0.0 - beta02"
 implementation "android.arch.work:work-runtime-ktx:$work_version"
}
Copy the code

You can here] (developer.android.com/topic/libra…). To the latest version of the library. If you want to use Java dependencies, remove “-ktx”.

Define what your work does

Before we link multiple tasks together, let’s focus on how to perform one job. I’m going to focus on uploading tasks. First, you need to create your own Worker implementation class. I’ll name our class UploadWorker and override the doWork() method.

Workers:

  • Define what your job actually does.
  • Take input and produce output. Inputs and outputs are represented as key-value pairs.
  • Always return a value indicating success, failure, or retry.

Here is an example of how to implement an image upload Worker:

class UploadWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {

    override fun doWork(): Result {
        try {
            // Get the input
            val imageUriInput = inputData.getString(Constants.KEY_IMAGE_URI)

            // Do the work
            val response = upload(imageUriInput)

            // Create the output of the work
            val imageResponse = response.body()
            val imgLink = imageResponse.data.link
            // workDataOf (part of KTX) converts a list of pairs to a [Data] object.
            val outputData = workDataOf(Constants.KEY_IMAGE_URI to imgLink)

            return Result.success(outputData)

        } catch (e: Exception) {
            returnResult.failure() } } fun upload(imageUri: String): Response {TODO(" Webservice request code here ") // Webservice request code here; note this would need to be run // synchronouslyfor reasons explained below.
    }

}
Copy the code

There are two points to note:

  • The input and output are as followsDataPass, which is essentially a mapping of primitive types to arrays.DataObjects should be fairly small — there is actually a limit to the total size that can be input/output. By theMAX_DATA_BYTESSettings. If you need to send more data in and outWorker, the data should be placed elsewhere, for exampleRoom database. As an example, I pass in the URI of the image above, not the image itself.
  • In the code, I show two return examples: result.success () and result.failure (). There is also a result.retry () option, which will retry your work at a later time.

Define how your Work should work

On the one hand, Worker defines what the work does, and on the other, WorkRequest defines how and when the work should run.

Here is an example of creating OneTimeWorkRequest for UploadWorker. There can also be repetitious PeriodicWorkRequest:

// workDataOf (part of KTX) converts a list of pairs to a [Data] object.
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)

val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
        .setInputData(imageData)
        .build()
Copy the code

This WorkRequest will enter the imageData: Data object and run as soon as possible.

Assume UploadWork shouldn’t always run immediately — it should only run when the device has a network connection. You can do this by adding Constraints objects. You can create constraints like this:

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

Here are other examples that are subject to support constraints:

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

Finally, remember result.retry ()? As I said earlier, if the Worker returns result.retry (), the WorkManager will reschedule the work. You can customize the retreat conditions when creating a new WorkRequest. This allows you to define when the run should be retried.

The retreat condition is defined by two attributes:

  • BackoffPolicy, which defaults to exponential but can be set to linear.
  • Duration. Default value: 30 seconds.

The following combination of code is used to queue upload work, including constraints, inputs, and custom fallback policies:

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

// Define the input
val imageData = workDataOf(Constants.KEY_IMAGE_URI to imageUriString)

// Bring it all together by creating the WorkRequest; this also sets the back off criteria
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
        .setInputData(imageData)
        .setConstraints(constraints)        
        .setBackoffCriteria(
                BackoffPolicy.LINEAR, 
                OneTimeWorkRequest.MIN_BACKOFF_MILLIS, 
                TimeUnit.MILLISECONDS)
        .build()
Copy the code

Run the work

That’s all very well, but you haven’t really scheduled your work to run. Here is a line of code that tells WorkManager to schedule work:

WorkManager.getInstance().enqueue(uploadWorkRequest)
Copy the code

You first need to get an instance of the WorkManager, which is a singleton responsible for performing your work. Enqueue is called to start the entire process of WorkManager tracing and scheduling work.

Behind the scenes — how does the work work

So, what can WorkManager do for you? By default, WorkManager will:

  • Off main threadRun your work (assuming you are inheritingWorkerClass, as aboveUploadWorkerShown below).
  • Guarantee that your work will run (it won’t forget to run your work even if you reboot the device or the application exits).
  • Run according to best practices at the user API level (described in the previous article).

Let’s take a look at how The WorkManager ensures that your work runs off the main thread and executes. Behind the scenes, WorkManager includes the following sections:

  • Internal TaskExecutor: A single-threaded Executor that handles all queued requests for work. If you’re unfamiliar with Executors, you can read more about it here.

  • WorkManager database: A local database that tracks all information and status for all jobs. This includes the current state of the work, the inputs and outputs of the work, and any constraints on the work. This database enables WorkManager to ensure that your work gets done — if your user’s device is restarted and the work is interrupted, all the details of the work can be extracted from the database and restarted when the device is restarted.

  • WorkerFactory: A default factory for creating instances of workers. We’ll explain why and how to configure it in a future blog post.

  • Default Executor: A Default Executor that runs your work unless you specify otherwise. This ensures that by default, your work is run synchronously and outside the main thread.

  • These parts can be rewritten to have different behaviors.

Android Developer Conference Showcase 2018

When you schedule WorkRequest:

  1. Internal TaskExecutor instantly sends yourWorkRequestThe information is saved to the WorkManager database.
  2. Later, when satisfiedWorkRequestConstraints“(which can happen immediately), the Internal TaskExecutor will tellWorkerFactoryTo create aWorker.
  3. After that, the defaultExecutorCall yourWorkerdoWork()methodsOff main thread.

In this way, by default, your work is guaranteed to run off the main thread.

Now, if you want to use some mechanism other than the default Executor to run your work, that’s fine! Out-of-the-box support for CoroutineWorker and RxJava (RxWorker) as a means of working.

Alternatively, you can use ListenableWorker to specify exactly how the work will be performed. Worker is actually an implementation of ListenableWorker, which runs your work on the default Executor by default and is therefore synchronized. So, if you want to have full control over the threading policy of work or run work asynchronously, you can subclass ListenableWorker (details will be discussed in a later article).

WorkManager saves all the work information in a database, but it does it anyway, which makes it a good fit for tasks that need to be guaranteed. This is what makes it easy for WorkManager to handle tasks that need no safeguard and only need to be executed on background threads. For example, suppose you have downloaded an image and want to change the color of the UI section based on that image. This is work that should be run off the main thread, but because it is directly related to the UI, you do not need to continue if you close the application. So in cases like this, don’t use WorkManager — stick with something lightweight like a Kotlin coroutine or create your own Executor.

Use chains for dependency work

Our filter example involves more than one task — we want to filter multiple images, then compress and upload. If you want to run a series of WorkRequests one after another or in parallel, you can use chains. The example figure shows a chain with three filter tasks running in parallel, followed by the compression task and the upload task, running in order:

Working with WorkManager is simple. Assuming you’ve created all WorkRequests with the appropriate constraints, the code looks like this:

WorkManager.getInstance()
    .beginWith(Arrays.asList(
                             filterImageOneWorkRequest, 
                             filterImageTwoWorkRequest, 
                             filterImageThreeWorkRequest))
    .then(compressWorkRequest)
    .then(uploadWorkRequest)
    .enqueue()
Copy the code

Three image filter WorkRequests were executed in parallel. Once all filter WorkRequests have been completed (and only all three have been completed), compressWorkRequest, then uploadWorkRequest, occurs.

Another advantage of the chain is that the output of one WorkRequest serves as the input of the next WorkRequest. So, assuming you set up the input and output data correctly, as my UploadWorker example did above, these values will be passed automatically.

To process the output of three filter work requests in parallel, you can use an InputMerger, especially ArrayCreatingInputMerger. The code is as follows:

val compressWorkRequest = OneTimeWorkRequestBuilder<CompressWorker>()
        .setInputMerger(ArrayCreatingInputMerger::class.java)
        .setConstraints(constraints)
        .build()
Copy the code

Note that the InputMerger is added to the compressWorkRequest, not the three filter requests in parallel.

Assume that the output of each filter work request is the key “KEY_IMAGE_URI” mapped to the image URI. Adding an ArrayCreatingInputMerger serves as an output of parallel requests, and when those outputs have matching keys, it creates an array of all the output values, mapped to a single key. The visualization is as follows:

ArrayCreatingInputMerger function visualization

Therefore, the input of compressWorkRequest will eventually become a “KEY_IMAGE_URI” pair mapped to the filter image URI array.

Observe your WorkRequest status

The easiest way to monitor your work is to use the LiveData class. If you’re not familiar with LiveData, it’s a lifecycle aware, surveillable data holder — described in more detail here.

GetWorkInfoByIdLiveData returns a WorkInfo LiveData. WorkInfo contains the output data and enumerations that represent the working status. When the work completes successfully, its State is SUCCEEDED. So, for example, you could write some monitoring code to automatically display the image when the work is done:

// In your UI (activity, fragment, etc)
WorkManager.getInstance().getWorkInfoByIdLiveData(uploadWorkRequest.id)
        .observe(lifecycleOwner, Observer { workInfo ->
            // Check if the current work's state is "successfully finished" if (workInfo ! = null && workInfo.state == WorkInfo.State.SUCCEEDED) { displayImage(workInfo.outputData.getString(KEY_IMAGE_URI)) } })Copy the code

A few things to note:

  • eachWorkRequestThere is aUnique id, the unique ID is the lookup associationWorkInfoIs a way of.
  • WorkInfoThe ability to monitor and be notified of changes isLiveDataThe functionality provided.

Jobs have a life cycle represented by different states. You will see these states when monitoring LiveData

; For example, you might see:

Happy Path or working status

The “Happy path” of working status experience is as follows:

  1. BLOCKED: This state occurs only if the job is in the chain and is not the next job in the chain.
  2. ENQUEUEDWork enters this state as long as it is next in the work chain and is qualified to run. That work may still be waitingConstraintBe met.
  3. RUNNING: In this state, work is running. forWorker, which meansdoWork()Method has been called.
  4. SUCCEEDED: whendoWork()returnResult.success()When the work enters this final state.

Now, when the work is RUNNING, you can call result.retry (). This will cause work to fall back to the ENQUEUED state. Jobs can be CANCELLED at any time.

If the job runs with result.failure () instead of success. Its state will end in FAILED, so the complete flowchart for the state looks like this:

(From: Working with WorkManager Android Developer Summit 2018)

For a great video presentation, check out the WorkManager Android Developer Summit Presentation.

conclusion

That’s the basics of the WorkManager API. Using the code snippet we just introduced, you can now:

  • Create one that contains input/outputWorker.
  • useWorkRequest,Constraint, enable input and exit policy configurationWorkerThe operation mode of.
  • schedulingWorkRequest.
  • Understand the defaultWorkManagerBehind the scenes work on threads and safeguards.
  • Jobs that create complex chain interdependencies can be run sequentially and in parallel.
  • useWorkInfoMonitor yourWorkRequestIn the state.

Want to try WorkManager for yourself? Look at codelab, which contains Kotlin and Java code.

Stay tuned for more blog posts on the WorkManager topic as we continue to update this series. Any questions or anything you’d like us to write about? Let us know in the comment section!

Thank youPietro Maggi

WorkManager related resources

  • The official documentation
  • Reference guide
  • The WorkManager 1.0.0 – beta02 Release notes
  • Codelab: Kotlin and Java.
  • Source code (part of AOSP)
  • Working with WorkManager (Android Developer Summit 2018) Presentation
  • Issue Tracker
  • WorkManager on StackOverflow
  • Google’s Power blog post series

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.