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
Links
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