Jetpack Workmanager

Jetpack Workmanager

Workmanager outpaces hard labour

Are you building an app with loads of background processes? Would you like to target 90% of the international market? Do you want to avoid hours of work building it all from scratch? Of course you do. But how?

Over the years, Google have provided various methods for developers to assist in doing or scheduling work in the background. JobScheduler is a great new API, but is only supported for Android Lollipop (API 21) and above. Firebase JobDispatcher is backward compatible as far as Ice Cream Sandwich (API 15)—but it requires Google Play services, which is big and heavy and not available in China. AlarmManager works on all API levels, but requires BroadcastReceiver to survive device reboots and also is subject to some power manager restrictions, which got introduced in Android P. So it looks like we have a choice between excluding older devices, or doing hours of work building it all from scratch.

Jetpack to the rescue!

At this year’s I/O conference, Google launched Android Jetpack: a bundled group of APIs and services designed to accelerate Android app development. Some of these were new, some existed already, but all are going to remove a great deal of development tedium for us.

WorkManager is effectively a wrapper around the previous services with a bunch of convenience methods. It chooses the best APIs available based on a device’s capabilities.

This means that developers get the best of all worlds: the most advanced APIs where they are available, and simpler ones where they aren’t. No more conditional logic in order to do this ourselves!

What makes it lovely?

WorkManager supports API level 14 and above (around 90% of the app market). It’s also independent of Google Play services, meaning it can be used on devices which don’t have it (such as most devices in the People’s Republic of China).

It follows system health best practices, and won’t cause excessive battery drain: for example, it respects system background restrictions, like doze mode: it wont wake up your phone just to do this work.

It also has constraint awareness, so tasks can be customised only to run when certain conditions are met, like network availability, storage space, or charging status.

It can be used to perform synchronous, asynchronous, one-off or repeated tasks. Complex work requests can even be run concurrently or daisy-chained, with the output from one work request used as input for the next.

LiveData (another JetPack feature for observable data) is supported, allowing you to easily display the worker’s current state in your app, for example in a progress bar. And if the API is queued, we can actually query its current state too (e.g. Running, Success or Failure).

Using it the right way

The perfect use case for WorkManager is when we need a guaranteed, but non-immediate execution of a task—one that can be deferred or executed opportunistically (ie, as soon as practically possible).

Some usual background tasks like this we come across everyday are sending logs, uploading images and videos, or periodically syncing local data with the network.

A peek into the technical implementation

WorkManager relies on a number of classes to perform its magic; here’s an introduction to them.

Worker

The class that does all the work. We just need to extend the abstract Worker class, override the doWork() method and write all the business logic here.

class SampleWorker : Worker() {
  override fun doWork(): Result {
    processSample() return Result.SUCCESS;
  }
}

The WorkManager runs a Worker on a background thread by default. If we are already running on a background thread and have a need for a blocking call to WorkManager, we can use the SynchronousWorkManager class.

There are a number of component parts to a Worker, which we will deal with in the next few sections.

WorkRequest

WorkRequest helps us to stipulate the types of tasks and schedule them.

We have two options here:

OneTimeWorkRequest

The most commonly required request, for one-off tasks.

e.g: Fire and forget API calls, get the user’s current location details, etc.

PeriodicWorkRequest

Suitable for recurring tasks. e.g: syncing server data with the local storage, pushing app analytics to the server etc.

val sampleWork = OneTimeWorkRequestBuilder<SampleWorker>().build()
WorkManager.getInstance().enqueue(sampleWork)

Naming the WorkRequests

WorkRequests are identified by a UUID which gets assigned to them by default, eg

  b9474d49-a87c-49c5-a72f-9e047bfc536c

This isn’t very readable, but we can tag them with meaningful names too:

val reqBuilder = OneTimeWorkRequest.Builder(SampleWorker::class.java)
  .addTag(TAG_SAMPLE_WORK)

Constraints

With every WorkRequest, we can also optionally specify any number of Constraints which determine when the worker should be enqueued.

So, what if we need a network connection to perform our task, and can’t lose connectivity during or before task execution? In this case, we can use NetworkType.CONNECTED (which only allows execution when a network is connected).

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

To read which constraints a WorkRequest has, use:

getRequiredNetworkType(): NetworkType
requiresBatteryNotLow(): Boolean
requiresCharging(): Boolean
requiresDeviceIdle(): Boolean
requiresStorageNotLow(): Boolean

To set constraints, use:

setRequiredNetworkType(NetworkType requiredNetworkType)
setRequiresBatteryNotLow(boolean requiresBatteryNotLow)
setRequiresCharging(boolean requiresCharging)
setRequiresDeviceIdle(boolean requiresDeviceIdle)
setRequiresStorageNotLow(boolean requiresStorageNotLow)

NetworkType is an enumerable that can take the values:

CONNECTED, METERED, NOT_REQUIRED, NOT_ROAMING, UNMETERED

WorkStatus

If we want to show the progress of our WorkManager on the UI, for example, showing a progress bar during a server update and hiding it once it is done, we can specify WorkStatus as a LiveData observable (from Android Architecture Components), which can easily be mapped to any UI component.

var outputStatus: LiveData<List<WorkStatus>> outputStatus =
  workManager.getStatusesByTag(TAG_SAMPLE_WORK)

Work manager also provides a WorkStatus object which helps us to track the following statuses:

ENQUEUED, RUNNING, SUCCEEDED, FAILED, BLOCKED, CANCELLED

WorkContinuation for sequential and concurrent execution 

If we want to perform a chain of tasks (similar to RxJava), or run several tasks in a specific order, WorkManager allows us to run them in sequence with the WorkContinuation class.

For every chain, we can specify a uniquely identifiable name.

Now, what to do with existing work with the same unique tag in case of a pileup?

WorkManager lets us specify constraints for these cases too with some ExistingWorkPolicy enumerations:

APPEND: Append new work as a child of that work sequence.

KEEP: If there is existing work with the same tag, do nothing.

REPLACE: Replace existing work with new work with the same tag.

WorkManager.getInstance()
  .beginUniqueWork(
    TAG_UNIQUE_WORK_CHAIN_NAME,
    ExistingWorkPolicy.REPLACE,
    OneTimeWorkRequest.from(GetConfigWorker::class.java)
  )

To chain the tasks, beginWith() is used for the first request, and then we chain them by using the then() operator. Each then() returns a new WorkContinuation instance.

WorkManager.getInstance()
  .beginWith(getServerConfigWorkRequest)
  .then(processServerResponseWorkRequest)
  .then(updateServerWorkRequest)
  .enqueue()

Of course, we can also perform tasks in parallel. In the below example, getServerConfigWorkRequest1 and getServerConfigWorkRequest2 are scheduled to run at the same time.

WorkManager.getInstance()
  // parallel execution
  .beginWith(getServerConfigWorkRequest1, getServerConfigWorkRequest2)
  .then(processServerResponseWork)
  .then(updateServerWork)
  .enqueue()

Setting Input and Output Data for a Worker

Every Worker’s outputs can become inputs for its children, by using the setOutputData() and setInputData() methods.

val output: Data = mapOf(KEY_RESULT to valueOfResult)
  .toWorkData()
setOutputData(output)

InputMerger

With InputMerger, we can merge the data outputs from different Workers, combine them into a single object, and also feed it as input data to the following Worker.

OverwritingInputMerger

Input Merger 1

This is the default type of InputMerger.

In case of a collision of two data values with same keys, this merger will replace the old value for that key with the newly arrived one. For example:

A first Worker outputs:

KEY_FILM: filmValue1
KEY_MUSIC: musicValue1

A second worker then outputs

KEY_FILM: filmValue2
KEY_DOCUMENTARY: docValue1

The InputMerger of the two outputs

KEY_FILM: filmValue2
KEY_MUSIC: musicValue1
KEY_DOCUMENTARY: docValue1

ArrayCreatingInputMerger

Input Merger 2

If we have a use case where overwriting is not suitable and we need to gather all the results of workers into an array, then ArrayCreatingInputMerger can be used. For example,

A first Worker outputs

KEY_FILM: filmValue1
KEY_MUSIC: musicValue1

A second worker then outputs

KEY_FILM: filmValue2 
KEY_DOCUMENTARY: docValue1

The InputMerger of the two outputs

KEY_FILM: [filmvalue1, filmValue2]
KEY_MUSIC: musicValue1
KEY_DOCUMENTARY: docValue1

The ArrayCreatingInputMerger documentation starts with a great explanation of this flow.

Some anomalies to consider

PeriodicWork

The minimum period length is 15 minutes, which is the same as JobScheduler. It’s not possible to set an initial delay. It’s subject to Doze and other OS background restrictions. It cannot be chained.

Worker input/output data

The Data object isn’t very large! Individual data elements are limited to 10KB each in serialised form.

It is suitable for light to intermediate data, file URIs, small amounts of text etc.
Use Room from JetPack Architecture Components instead for larger data!

When should we use WorkManager?

Now that we have some idea on how to use a WorkManager, it is really important to know when to use it.

It is very easy to go wrong in choosing the right service.

In most scenarios, what we need to do falls into one of the following three categories.

Best-effort execution at exact/deferrable timing

A reasonable effort should be taken to execute the process at the specified time, but it is OK if it fails. For example: updating an ImageView based on an API call.

This should use a thread pool, RxJava or Kotlin Co Routines, as it is foreground UI work and it doesn't need to survive process death.

Guaranteed execution at exact timing

The job should be executed at a given specified time without any mitigation. For example: process a payment transaction.

The user hits a transaction button; we want to process the transaction, and also update the UI based on the process status.

As per Google’s documentation :

A Foreground service is a service that the user is actively aware of and isn't a candidate for the system to kill when low on memory.

So, this should definitely be a Foreground service, as app cannot be killed by the system while the transaction is happening.

Guaranteed eventual execution

It is OK if execution doesn’t happen at a specified time, but we must execute it without fail. For example: sending logs to the server periodically.

This is a perfect use case for WorkManager, as we want a guaranteed execution which can be postponed, for example, doze mode can kick in, and it is still OK to send our logs later.

Verdict

I made a sample app to experiment with WorkManager. You can check out the source on Github.

After testing it, it became clear that it is quite buggy as it is still in alpha. However, while testing, Google upgraded WorkManager twice (which is great news—they’re working hard on finishing it). We can track the issues here.

So, our hero, WorkManager, is on its way—but it’s not ready yet for use in production. However, if they keep updating at the same rapid rate, hopefully it should be soon—so hold on for Worker a little longer, it’s guaranteed to execute eventually!

Author: Badhri Canessane

Life at Somo

Contact Us

London Office

18th Floor Portland house
Bressenden Place
Victoria
SW1E 5RS

+44 (0)20 3397 3550

Bristol Office

1 Temple Way
Bristol
BS2 0BY


+44 (0)117 214 0910

Get in touch