WorkManager is a new API in Android Architecture Components introduced in the Google I/O 2018. It simplifies and makes it much easier to do work on background threads. The WorkManager schedules tasks as instances of the Worker class and can schedule these workers based on certain conditions which you can set by using the provided The Constraints class. Examples of conditions you can set from the Constraints class, can be things like available internet/wifi connection or if a charger is connected. The WorkManager can also schedule all Worker instances to launch in any order and we can also pass input data into a Worker and get the output data.

Also a very important note about WorkManager: “WorkManager is intended for tasks that require a guarantee that the system will run them even if the app exits…”*

How to use it

First you have to add the dependency in your build.gradle file: implementation "android.arch.work:work-runtime:1.0.0-alpha02"

We will look at the following:

  • How to create a Worker
  • How to create Constraints
  • How to give a Worker instance an input and get its output
  • How to schedule all Worker’s to be executed in a particular order

User story

To demonstrate how to use WorkManager we will start by establishing a made-up user story. In our imaginary app QuickSnapper, we press a shutter button and the app will take a picture and apply some stickers on it and upload it all in one automatic process thanks to the WorkManager.

So let’s split up the user story in 3 use cases:

  • 1 The user takes a photo (We want to compress it here)
  • 2 The app adds weather and location information on the picture after step 1 (GPS and Internet must be available)
  • 3 The app uploads the image immediately after step 1-2 (Internet must be available)

For each use case we will make a Worker class. To do that we need to make a class and extend Worker which requires us to implement a doWork() method with a return type of a WorkerResult that can either be WorkerResult.SUCCESS or WorkerResult.FAILURE

Creating Workers

The first Worker compress our Bitmap into a smaller size, convert the Bitmap to ByteArray passes it in the WorkManager’s outputData object and then we return WorkerResult.SUCCESS or WorkerResult.FAILURE.

    class ImageCompressionTask(val bitmap: Bitmap?) : Worker() {
        override fun doWork(): WorkerResult {
            val newBitmap: Bitmap?
            try {
                //Create a compressed bitmap
                newBitmap = Bitmap.createScaledBitmap(bitmap, 500, 500, false)
                return WorkerResult.SUCCESS
            } catch (e: IllegalArgumentException) {
                return WorkerResult.FAILURE
            }
        }
    }

In the second Worker we retrieve the Bitmap from the Workers inputData object and adds some weather and location stickers on the image

    class AddStickersTask : Worker() {
        override fun doWork(): WorkerResult {
            try {
                //Adding stickers on the bitmap...
                return WorkerResult.SUCCESS
            } catch (e: Exception) {
                return WorkerResult.FAILURE
            }
        }
    }

In the last Worker we just upload our Bitmap to our server

    class UploadImageTask : Worker() {
        override fun doWork(): WorkerResult {
            //Retrieve bitmap and upload work here
            return WorkerResult.SUCCESS
        }
    }

Creating Constraints for workers

Now we have created 3 Worker classes and can chain them together so they run when each previous Worker has returned WorkerResult.SUCCESS. The WorkManager won’t proceed if any of the Worker instances returns WorkerResult.FAILURE.

But first we have to make our Constraints for our Worker instances, so the Worker only runs if the conditions we have set in the Constraints class is met.

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

GPS requirement is not yet supported in the Constraints class but we will instead check for enabled GPS in the AddStickersTask and if it’s not enabled we will return FAILURE and the next WorkManager won’t proceed to the next Worker

Now lets use our Worker classes

Here we set our Constraints for each Worker and build() will return an instance of OneTimeWorkRequest

val imageCompressionTask = OneTimeWorkRequest.Builder(ImageCompressionTask::class.java).build()
val addStickersTask = OneTimeWorkRequest.Builder(AddStickersTask::class.java).setConstraints(constraint).build()
val uploadImageTask = OneTimeWorkRequest.Builder(UploadImageTask::class.java).setConstraints(constraint).build()

We make them as OneTimeWorkRequest because we only want these Worker to execute once. PeriodicWorkRequest can be used in cases where you want a Worker for some repetitive work which can run in intervals you can set.

Input data and output data

Input data

As it is right now, our ImageCompressionTask doesn’t have any Bitmap to compress. So we have to give it a Bitmap before it begins its work. We can pass a Bitmap or any type of data to our Worker classes by sending it an instance of a Data object from the WorkerManger API.

The Data class dosn’t support Bitmap but it does support ByteArray, so we can convert our Bitmap to a ByteArray by using this static method to create a new intance of Data containg our ByteArray

val compressionData = Data.fromByteArray(getBitmapByteArray())

Now we just add a String tag to identify and retrieve our Worker later and we give it the compressionData like this:

 val imageCompressionTask = OneTimeWorkRequest.Builder(ImageCompressionTask::class.java).addTag(TAG_WORKER_1).setInputData(compressionData).build()

Getting the input data in the Worker class

Now when we have given our ImageCompressionTask some input data, we can extrat that by just calling the inputData object provided from the Worker class and when we are finished with our data, we can make it put in the outputData object so we can retrieve it outside of the ImageCompressionTask class

class ImageCompressionTask() : Worker() {

    override fun doWork(): WorkerResult {
        try {
             //get the input data
            val bitmapByteArray = Data.toByteArray(inputData)
            val bitmap = BitmapFactory.decodeStream(ByteArrayInputStream(bitmapByteArray))

            val newBitmap = Bitmap.createScaledBitmap(bitmap, 500, 500, false)

            //Save the bitmap back to the outputData
            outputData = Data.fromByteArray(getBitmapByteArrayHelper(newBitmap))
            return WorkerResult.SUCCESS
        } catch (e: IllegalArgumentException) {
            return WorkerResult.FAILURE
        }
    }

Output data

Usually we want to get the output data from a Worker when it have finished its work. To do that we can listen on a specific Worker by retrieving the Worker by its tag:

     WorkManager.getInstance().getStatusesByTag(TAG_WORKER_1).observe(this, Observer { workerStatusList ->
            val workstatus = workerStatusList?.get(0)
            workstatus?.let {
                if (it.state.isFinished) {
                    val outputData = it.outputData
                }
            }
        })

Putting everything together

Now we just feed our WorkManager with our Worker’s in the order as described in our user story and we done!

WorkManager.getInstance().beginWith(imageCompressionTask).then(addStickersTask).then(uploadImageTask).enqueue()

When should you use it?

The WorkManager is very useful for tasks running in background threads and for tasks which need to fulfill certain conditions before they can run or automated tasks running in a certain order.

Some example of when WorkManager also can be really useful

  • Uploading data
  • Download data
  • Bitmap Compression work
  • GPS location logging.
  • Chat apps
  • Playlists apps
  • Repetitive work that needs to run on background threads

A more detailed and advanced tutorial on how to work with WorkManager from Google: https://codelabs.developers.google.com/codelabs/android-workmanager/#0

More about WorkManager: https://developer.android.com/topic/libraries/architecture/workmanager

Article Photo by Annie Spratt

android workmanager workers jetpack threads

Author

Muddi Walid

Android Developer

You may also like

Higher order functions in Swift

Higher order functions in Swift are extremely powerful tools to have in your developer toolkit, the only issue is that it might take some time to get comfortable with them. Before we get started, let’s have a look at some important terms to know in relation with higher order functions:...

iOS
Join the dark side and implement Dark Mode in iOS

WWDC this year brought to light a lot of amazing features Apple has been working on lately. One of these features, and maybe one of the most expected, was Dark Mode support.

iOS