WorkManager Tutorial for Android: Getting Started

by · August 30, 2018

At Google I/O 2018, Google announced the WorkManager library as part of the Android Jetpack component set. Moving forward, WorkManager replaces JobScheduler as Google’s recommended way to enqueue background tasks that are guaranteed to execute.

In this WorkManager tutorial, you’ll learn about the different types of background work and their respective uses. You’ll also implement a series of workers, each performing a different task. Finally, you will connect the tasks together using WorkManager task chaining.

Understanding Background Work

Background work is any app-related task that is not executed on the Android main (UI) thread.

Usually, you need to execute on a background thread because the task is an expensive operation — like applying filters to a bitmap — or the task depends on a network request to query or post data.

Because you don’t want to block the main thread, or you want the task to continue running even if you close the app, you dispatch the task to a background thread.

Types of Background Work

The types of background work can be categorized in two dimensions:

The vertical axis is the timing of the work: “Does the work need to be executed at an exact time or can it be deferred?”

The horizontal axis represents how relevant the work is: “Do you need a guarantee that the work will be executed?”

Depending on your use case, there are various approaches available:

On the left, for tasks that don’t need a guarantee of execution, you can use ThreadPools, RxJava or coroutines.

On the upper right, for exact timing and guaranteed execution, you should use a ForegroundService.

On the lower right, for deferrable and guaranteed execution, you have several options: JobScheduler, Firebase JobDispatcher, and Alarm Manager + Broadcast receivers.

WorkManager falls into the category of guaranteed execution and deferrable.

Why WorkManager?

Since Android Marshmallow, the Android team has started to put more focus on battery optimizations. The team introduced Doze mode and, later in Android Oreo, it introduced background service limits — just to mention a few. With Android Pie, the team continues to give attention to optimization and introduce new changes.

Until now, to perform background work, developers not only had to know about these battery optimizations, but also had to choose between the many ways to implement the work.

Depending on the configuration of a given device that your app is installed on (OS version, Google Play services availability, etc.), WorkManager will choose an appropriate option and execute the work when it considers the optimum time to do so.

In doing so, WorkManager provides a simple and clean interface and “hides” the complexity of deferrrable but guaranteed background work from developers.

Knowing When to Use WorkManager

As was previously mentioned, WorkManager falls into the deferrable guaranteed execution category of background work.

A few example use cases:

Uploading files to a server.

Syncing data to/from a server and saving it to a Room database.

Sending logs to a server.

Executing expensive operations on data.

In this tutorial, you’ll learn how to execute these types of background tasks using WorkManager.

Getting Started

The project you’ll be working with, PhotoUploader, is an app to pick photos from your photo library and execute the following operations in the background:

Apply a sepia filter to each image.

Zip the filtered images.

Upload the .zip file to a server.

Use the Download materials button at the top or bottom of this tutorial to download the starter project.

Once downloaded, open the starter project in Android Studio 3.2 Beta 1 or newer. When you build and run it, you’ll see the first screen shown here:

Currently, tapping on the “Pick Photos” button will let you pick one or more photos from your library. After that, it does nothing. You’ll write code to add the featured mentioned above to the app.

Reviewing the project, you’ll see that it consists of two files:

MainActivity.kt: This is the main screen with a Pick Photos button.

ImageUtils.kt: Contains the aforementioned operations that you’ll execute in the background.

Setting Up the WorkManager Library

Open the build.gradle file for the app module and add the following dependency:

implementation "android.arch.work:work-runtime:1.0.0-alpha02"

Note: At the time of the writing of this tutorial, there’s an alpha04 version. However, parallel and chaining WorkRequests (to be explained later) won’t work. This is likely because the library is still alpha.

That’s all the setup you need to start using the library!

Understanding WorkManager Classes

Before adding code to the project, you’ll need to familiarize yourself with the following main classes that the WorkManager library contains:

WorkManager: This is the main class that you’ll use to enqueue WorkRequests.

Worker: You’ll need to subclass this to perform the task that will run in the background.

WorkRequest: This represents the request of the task that will run. You’ll set the task through the Worker subclass. You can also specify constraints — for example, only run the task if the device has Internet connectivity. There are two main types of work requests: OneTimeWorkRequest, which executes the task just one time, and PeriodicWorkRequest, which executes your task periodically. The period of a PeriodicWorkRequest can be customized.

WorkStatus: Whenever you need to know the status — running, enqueued, finished, etc., for example — of a particular WorkRequest, you can ask the WorkManager for it. It will provide a LiveData object containing one or more WorkStatus objects.

Creating Your First Background Task

For your first background task, you’ll create the FilterWorker. This worker will receive the URIs of the selected images and apply a sepia filter to them.

Note: In the following snippets, file imports won’t be shown, as you’ll be focusing on the actual code required to implement tasks. The worker-related imports start with the package androidx so, when importing, make sure you choose the correct one.

Writing the FilterWorker Code

To start, create a new package called workers; this will hold the code required to finish your project. Next, add a FilterWorker.kt file to it with the following content:

Each worker has to extend the Worker class and override the doWork method, which returns a WorkResult. The result can succeed or fail, giving you feedback on the final outcome. Since the work being done can end with an exception, you’re wrapping the calls in a try-catch expression, and using the Kotlin single-line function syntax to return a value as the last line in each block of the try-catch.

This worker is made up of several steps. Going over each:

First, you get the image-related data from the inputData field bound within the worker, ultimately turning it into a bitmap:

You’ll see how this URI will be used in the next worker. Also, by returning a successful case, you notify that this worker has done its job without any issues. If there’s an exception, you return a failure to stop the work.

Note: inputData and outputData are just key-value maps. However, there is a 10KB limit for the payload.

Instantiating and Configuring the Worker

Now, open the MainActivity.kt file and replace the onActivityResult method with the following:

After the images are selected and returned, you wrap each into your FilterWorker using the buildSepiaFilterRequests() method that you’ll add next. Then you retrieve the WorkManager instance, and you begin your chain of tasks by applying sepia filters.

Since there are two ways to select images — by selecting a single one, and by choosing multiple — there’s also two ways to build requests. On the one hand, if there are multiple images selected, you have to run a for loop and map each image to a filter request:

Having finished all of that, you’re ready to try out the filters and see what you get!

Checking the Results

Run the app, pick one or more photos and then, after a few seconds, open the Device File Explorer via the following Android Studio menu: View ▸ Tool Windows ▸ Device File Explorer. Navigate to the /data/user/0/com.raywenderlich.android.photouploader/files/outputs folder.

Note: On different emulators, there are different output folders. So if you cannot find the /user/0 folder, try looking up /data/data.

You should see the bitmap files with the sepia filter applied:

If you don’t see them, try synchronizing:

Congratulations! Your first worker worked just fine! :]

If you want, you can delete all the files with the Device File Explorer but, later on, you’ll create a worker that cleans these files.

Chaining Tasks

After applying the sepia filter to each selected image, you’ll compress them into a single .zip file.

Writing the CompressWorker Code

Under the workers package, create a new file called CompressWorker.kt with this content:

You’ve combined the zip compression with the sepia filter code. After you created a CompressWorkerzipFiles, you chained it with the sepia filter worker by calling .then(zipFiles) on the workManager instance. You can chain arbitrary amounts of workers this way, and it’s really simple to do so!

Also notice how you don’t need to pass any arguments to the CompressWorker, because of the inputData and outputData constructs. When the first worker finishes, whatever output it passed on gets propagated to the next worker in chain.

Checking the Results

Run the app again. Select one or more images and, after a few seconds, check in the app files with Device File Explorer. Now, you should also see the .zip file.

Cleaning the Worker and Uploading the ZIP

Next up, you’ll clean the directory. Additionally, after generating the .zip file, you’ll upload it to a server.

Note: Since the UploadWorker needs to upload the file somewhere, you need a file server to be running. You can do this in two different ways: run one locally or upload to a shared service.

Both options are ready for you. There’s a shared service running to which you can upload at any time. If, however, you prefer to upload locally, you can use the prepared local version of a file server.

To run the local version, you need to install NodeJS, and then navigate in the terminal to the server folder, found in the materials you downloaded. Execute the command node app.js, and everything should be up and running.

Depending on the chosen server option, open ImageUtils.kt and comment out one of the two SERVER_UPLOAD_PATH constants and uncomment the other one. The one containing 10.0.2.2:3000 is used for local server, while the other one is the shared service. Feel free to change the local server IP address to something else, if you’re using a real device.

Checking the Results

Run the app, again. Next, select one or more images and, after a few seconds, open Logcat (View ▸ Tool Windows ▸ Logcat). You should see something like this, confirming that the file was correctly received by the server:onResponse - Status: 200 Body: {"url":"/files/Your-UUID-string.zip"}

If you are running the server locally, you can open a browser and go to http://localhost:3000/files/Your-UUID-string.zip to download the file.

Starting Unique Work

Worker tasks can be started in two different ways. First, you can call the beginWith() method, as you’ve done so far. The second way is by calling beginUniqueWork() instead. As the name suggests, the beginUniqueWork() method starts work that can only have one unique instance. But you also have to provide something called an ExistingWorkPolicy. If you try to start another work instance, the previous work will proceed according to the chosen policy — replace, keep or append:

Replace the existing sequence with the new one.

Just ignore the new one and keep the existing sequence.

Append the new sequence to the existing one.

Replacing Existing Work of Picked Photos

For this tutorial, you’ll replace the existing work. To do that, open MainActivity, add a constant to the companion object and change onActivityResult() as follows:

By calling beginUniqueWork(UNIQUE_WORK_NAME, ExistingWorkPolicy.REPLACE, cleanFiles), you’ve started unique work and decided to use the REPLACE policy. This means that, each time that you click the Pick Photos button and select one or more images, WorkManager will replace the existing sequence of WorkRequests and start a new one.

Observing Work

WorkManager also allows you to observe the progress of each work instance using the following:

getStatusById(): Use this to get a LiveData object containing the WorkStatus for the WorkRequest with a given ID.

getStatusesByTag(): Returns a LiveData object containing the list of WorkStatus objects for the specified tag. This is possible because you can tag multiple WorkRequests with the same name.

getStatusesForUniqueWork(): This will also return a LiveData object containing the list of WorkStatus objects that have the same unique work name.

Tagging a WorkRequest and Observing It

You’ll observe the work of the requests to toggle a progress bar and a cancel button. Open MainActivity and add a tag to the uploadZip work request using a new constant WORK_TAG:

getStatusesByTag() returns a LiveData object to which you attach an Observer. When you get an update on the status from the WorkManager, you check if it’s active or not. Since you’ve tagged only one worker, you can take the first item from the list (if it exists) and check its state. If it’s not finished, you show a progress bar and a cancel button. Otherwise, you hide them.

Now, at the bottom of the onCreate() lifecycle method in MainActivity, add a call observeWork().

Canceling the Picking Photos Work Sequence

Build and run the app, select some photos and then cancel the work requests using the cancel button.

Understanding Constraints

Some work cannot be done without a certain set of conditions. For example, work may only run if your device is not low on storage, is charging, etc. You can add constraints like these to any of your tasks. It makes sense to add a network-related constraint to the UploadWorker, because you cannot upload unless there is a stable connection.

As you can see, you’ve configured the upload request to only run if the device is connected to the network, otherwise it will not run until the condition is met. This is really convenient because you don’t have to implement the connection listeners and do the checks yourself.

Testing the Network Constraint

Run the app. Set the emulator/device into Airplane mode. Pick photos from your library and open the Logcat console. You should see that the UploadWorker will not run (filter the console for onResponse like you did before and check the timestamp on any results). Only after it receives a network connection will it upload the file. You can make sure this happens by taking a look at the console once again, after disabling Airplane mode.

Where to Go From Here?

Congratulations! You’ve just learned the basics of WorkManager.

You can download the final version of the project using the Download materials button at the top or bottom of this tutorial.

Here are some great references to learn more about the subject:

Feel free to share your feedback, findings or ask any questions in the comments below or in the forums. I hope you enjoyed this tutorial on WorkManager!