Exploring Jetpack: Scheduling tasks with Work Manager

In I/O ’18, Google Released Android Jetpack. It’s a set of libraries, tools and architectural guidance to help make it quick and easy to build great Android apps. In this Android Jetpack, Team at Google Release one Library Specifically designed for scheduling and managing the background tasks. It’s called “The WorkManager.

In this part you will learn about schedulers evolution matrix, what is work manager, how to use it and when to use it. In the
next part, you will learn about how to chain the works for ordered execution of tasks.

You may say at this moment that we have Job Scheduler API backed right inside of the Android OS for scheduling the tasks. We also have GCM network manager, Firebase Job Dispatcher, Evernote’s Android job and many other libraries that allows to schedule the background tasks. Why do we need another new library for scheduling the tasks?

To answer this answer, I came up with the background job scheduler evolution matrix. Consider this as the four pillars for the schedules.


🔥 Background Work Scheduler evolution matrix


Let’s discuss about each pillar one-by-one.

  1. Easy to schedule:
  • The background work scheduler should allow the application to schedule a task that can be run in specific conditions only. (e.g. The task should only run if the devices in charging.)
  • Once you schedule the task, you can forget about the task. The scheduler should provide a guarantee that the scheduled task will run whenever the required condition matches.
  • Each task can chain with another task to run multiple tasks parallelly or sequentially.

2. Easy to cancel:

  • You must have the control of over the task execution. The scheduler should provide APIs to cancel the scheduled task easily.

3. Easy to query:

  • Your application may be displaying the status of the task somewhere in the UI.
  • Let’s say you scheduled one task to upload the picture whenever the device is in charging and have the internet connection available. You want to display the upload process percentages in your UI.
  • The scheduler must provide API to get the current status of the task easily. If you can pass some result data whenever Task and that’s a bonus!!

4. Support for all android versions:

  • The scheduler APIs should work same across all the android versions. (I know that sounds impossible!!!)

Let’s the battle⚔️ begin. Let’s evaluate the work manager based on the evolution matrix.


🧐 What makes WorkManager APIs different⁉ ️

As per the official documentation,

  1. Easy to schedule:
  • The WorkManager API makes it easy to create deferrable, asynchronous tasks and also allows you to specify when they should run.
  • You can create a task and hand over that task to the work manager to run it immediately or whenever the device is under specific conditions (like “only while the device is charging and online”). It’s like fire-and-forget.
  • Work manager provides guarantee that your task will run when specific conditions match, even if your app is force-quit or the device is rebooted.

2. Easy to cancel:

  • WorkManager assigns UUID to the each work/task you schedule. Using this unique id, you can easily cancel any task at any time.

3. Easy to query:

  • You can ask for the status of the task — whether it is running, Pending or finished – by using the unique ID assigned to each task
  • Work manager APIs goes beyond only current state of the task and allows tasks to return data in key-value format.
  • Work manager uses LiveData to return the data and state of the work. So, your activity can observe this LiveData and it will get notified whenever the task is finished. (Hang tight!!! We will see this in detail.)

4. Support for all android versions:

  • Work manager APIs supports API 14 or above.
  • WorkManager chooses the appropriate way to run your task based on such factors as the device API level and the app state.
  • If the application is running, work manager APIs will new thread to run the task.
  • If the application process is not running, it will use JobScheduler APIs or Firebase Job dispatcher or Alarm manager API to run the scheduled task.

Thus, work manager APIs nails down all the four pillars of the evolution matrix. (Except the last one. Supporting all android Versions are next to impossible😄.)


Classes and Concepts:

Work manager APIs are built upon several classes. You have to subclass some of the abstract classes to schedule the task.

  • Worker: In the WorkManager world, Worker is equivalent to task or job that needs to be performed in the background. This is an abstract class. You need to subclass it. Your worker class contains the information about how to perform the task but it doesn’t have the information on when it will be run.
  • WorkRequest: It represents the work scheduling request. Each Work has to create WorkRequest before scheduling the Work. The WorkRequest will contain the unique id of the work, constraints explaining under which circumstances the task should be performed. This is an abstract class. The library provides two direct subclasses of this classes: OneTimeWorkRequest and PeriodicWorkRequest.
  • WorkManager: It is the class that manages and schedules the tasks based on the constraints defined in the WorkRequest.
  • WorkStatus: This class wraps the status of any work request. You can query the status of any work using the unique id of it’s request.

Workflow to schedule work 🧐:

Let’s understand how you can create and schedule new work.

1. Create the work:

  • Create one subclass of the Worker class. This class has one abstract method called doWork(). As the name suggests, you need to perform the work you want to do in the background. This method will be called in the background/worker thread. Write the program to perform the task in this method.
  • In the return, you have to return WorkerResult. Returning WorkerResult.SUCCESS indicates that the task you performed completed successfully. Returning WorkerResult.RETRY tells WorkManager to retry the work again. Returning WorkerResult.FAILURE indicates one or more errors occurred.
class DownloadWorker : Worker() {



// Define the parameter keys:

private val KEY_X_ARG = "X"

private val KEY_Y_ARG = "Y"

private val KEY_Z_ARG = "Z"



// The result key:

private val KEY_RESULT = "result"



/ * *

* This will be called whenever work manager run the work.

* /

override fun doWork(): WorkerResult {

// Fetch the arguments (and specify default values):

val x = inputData.getLong(KEY_X_ARG, 0)

val y = inputData.getLong(KEY_Y_ARG, 0)

val z = inputData.getLong(KEY_Z_ARG, 0)



val timeToSleep = x + y + z

Thread.sleep(timeToSleep)



/ /... set the output, and we're done!

val output = Data.Builder()

.putInt(KEY_RESULT, timeToSleep.toInt())

.build()



outputData = output

// Indicate success or failure with your return value.

return WorkerResult.SUCCESS

}

}Copy the code

2. Define contraints:

  • Define the constraints to tell the WorkManager when the work should be scheduled. If you don’t provide any constraints to your work request, the task will run immediately.
  • Here is the example constraint to schedule the work only when the device is charging and idle.
val myConstraints = Constraints.Builder()

.setRequiresDeviceIdle(true)

.setRequiresCharging(true)

.build()Copy the code

3. Create the work request.

  • You can create OneTimeWorkRequest to run work only one time.
val request = OneTimeWorkRequest.Builder(DownloadWorker::class.java)

.setConstraints(myConstraints)

.build()Copy the code
  • If you want to run the work to run periodically, create PeriodicWorkRequest and set the interval between to subsequent work.
val request = PeriodicWorkRequest

.Builder(DownloadWorker::class.java, 1, TimeUnit.HOURS)

.setConstraints(myConstraints)

.build()Copy the code

4. Schedule the request.

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

That’s it. You successfully scheduled your work. Pretty simple?


Input parameters and return values:

Setting up the input parameters:

  • While creating the work request you can pass the androidx.work.Data which contains the input arguments to the work. (You can imaging Data as special type Bundle, which holds the data in a key-value pair.)
  • Create new Data with all the required key-value pairs and set the data while creating the work request by calling setInputData().
val myData = Data.Builder()

.putLong(KEY_X_ARG, 42)

.putLong(KEY_Y_ARG, 421)

.putLong(KEY_Z_ARG, 8675309)

.build()

val request = OneTimeWorkRequest.Builder(DownloadWorker::class.java)

.setInputData(myData)

.build()Copy the code

Reading the input parameters and writing the output:

  • You can read all these input parameters by calling getInputData(). After performing the work, you can call setOutputData() to set the output Data for that worker.
class DownloadWorker : Worker() {



// Define the parameter keys:

private val KEY_X_ARG = "X"

private val KEY_Y_ARG = "Y"

private val KEY_Z_ARG = "Z"



// The result key:

private val KEY_RESULT = "result"



/ * *

* This will be called whenever work manager run the work.

* /

override fun doWork(): WorkerResult {

// Fetch the arguments (and specify default values):

val x = inputData.getLong(KEY_X_ARG, 0)

val y = inputData.getLong(KEY_Y_ARG, 0)

val z = inputData.getLong(KEY_Z_ARG, 0)



val timeToSleep = x + y + z

Thread.sleep(timeToSleep)



/ /... set the output, and we're done!

val output = Data.Builder()

.putInt(KEY_RESULT, timeToSleep.toInt())

.build()



outputData = output

// Indicate success or failure with your return value.

return WorkerResult.SUCCESS

}

}Copy the code

Observing the output data:

  • Work manager manages the LiveData of WorkStatus for all the work request. The application can observe that LiveData to get notify about whenever the work status changes.
  • You can also read the output Data, by calling getOutputData().
WorkManager.getInstance()

.getStatusById(request.id)

.observe(this@MainActivity, Observer {

it? .let {Copy the code
      // Get the output data from the worker.

val workerResult = it.outputData Copy the code
      // Check if the task is finished?

if (it.state.isFinished) {

Toast.makeText(this, "Work completed.", Toast.LENGTH_LONG)

.show()

} else {

Toast.makeText(this, "Work failed.", Toast.LENGTH_LONG)

.show()

}

}

})Copy the code

💭 When you should use work manager?

You might be thinking that work manager APIs are so easy to use. What if I use them to run all of my background tasks?

Well, keep in mind that work manager APIs are useful for the tasks that require guaranteed execution and they are deferrable. All the other use cases you can easily use intent services or foreground services.