Workout your tasks with WorkManager — Advanced Topics

“WorkManager is a library for managing deferrable and guaranteed background work.”

In my previous two posts about WorkManage I covered topics like:
  • Android memory model
  • Android battery optimizations
  • Current background processing solutions
  • Where is WorkManager placed in the background work schema
  • WorkManager components: Worker, WorkRequest and WorkManager
  • Constraints
  • Input/Output Data


In this blog post I’ll cover some extra features of the WorkManager library like:

  • how to identify a task
  • how to get the status of a task
  • BackoffPolicy
  • how to combine the tasks and the graphs of tasks (chaining the work)
  • how to merge the inputs and outputs
  • what are the Threading options in WorkManager

WorkManager mind map diagram

1️⃣ Identify a task

After a task (work) was created we will be interested in knowing the status of it, but in order to obtain this objective we should have some mechanisms that could be used to identify the task (work). There are 3 main ways that could be used to identify the work:

  1. Unique id (UUID): the id associated to the WorkRequest is generated by the library and it is not developer-friendly
  2. Tag: a task could contain many tags
  3. Unique name: a task could have only one unique name
UUID


UUID syncWorkId = syncOnlyOnceWork.getId();

Tagging work


val syncOnlyOnce = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(userIdData)
.addTag(Constants.WORKER_SYNC)
.addTag(Constants.ONE_SYNC)
.build()

Unique work sequence


WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
Constants.UNIQUE_NAME,
ExistingPeriodicWorkPolicy.KEEP,
syncPeriodically
)

Unique work chain


WorkManager.getInstance(context)
.beginUniqueWork(Constants.UNIQUE_NAME, ExistingWorkPolicy.REPLACE, task1)
.then(task2)
.then(task3)
.enqueue()

2️⃣ Get the status of a task

WorkInfo contains info about a particular WorkRequest

Deloitte - Workout your tasks with WorkManager

Deloitte - Workout your tasks with WorkManager (1)

By having the possibility to identify a task we are able to know more about its status by using LiveData or we also have the possibility to cancel it.

WorkManager & LiveData = ❤️


// by id
WorkManager.getInstance(context).getWorkInfoByIdLiveData(syncOnlyOnce.id)
.observe(this, Observer { workInfo ->
if (workInfo != null &&
workInfo.state == State.SUCCEEDED) {
displayMessage("Sync finished!")
}
})
//by tag
WorkManager.getInstance(context)
.getWorkInfosByTagLiveData(Constants.TAG_SYNC)
.observe(this,
Observer<List<WorkStatus>> { workStatusList ->
val currentWorkStatus = workStatusList ?.getOrNull(0)
if (currentWorkStatus ?.state ?.isFinished == true) {
displayMessage("Sync finished!")
}
})
//by unique name
WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkLiveData(Constants.UNIQUE_NAME)
.observe(this,
Observer<List<WorkStatus>> { workStatusList ->
val currentWorkStatus = workStatusList ?.getOrNull(0)
if (currentWorkStatus ?.state ?.isFinished == true) {
displayMessage("Sync finished!")
}
})

Cancel a task 


WorkManager.getInstance(context).cancelAllWorkByTag(Constants.TAG_SYNC)
WorkManager.getInstance(context).cancelUniqueWork(Constants.UNIQUE_NAME)
WorkManager.getInstance(context).cancelWorkById(UUID.fromString(Constants.UNIQUE_UDID))

3️⃣ WorkManager Policies

If we want to retry the work in some specific conditions, then the doWork method should return Result.retry() so the work is rescheduled according to a backoff delay and backoff policy

    • Backoff delay – the minimum amount of time to wait before retrying the work after the first attempt; should be >= 10 s (MIN_BACKOFF_MILLIS)
    • Backoff policy – the way the retry interval will increase over time for subsequent retry attempts (LINEAR or EXPONENTIAL)
    • Default policy is EXPONENTIAL with a delay of 10 seconds

❗ Existing Work Policy enums

  • KEEP — keeps the existing unfinished WorkRequest. Enqueues it if one does not already exist.
  • REPLACE — always replace the WorkRequest. Cancels and deletes the old one, if it exists.
  • APPEND — appends work to an existing chain or create a new chain.
  • APPEND_OR_REPLACE – the new work to run regardless of the status of the existing work

KEEP + REPLACE + APPEND = ExistingWorkPolicy

KEEP + REPLACE = ExistingPeriodicWorkPolicy

BackoffPolicy enum

EXPONENTIAL — Used to indicate that WorkManager should increase the backoff time exponentially

LINEAR — Used to indicate that WorkManager should increase the backoff time linearly

For a BackoffPolicy of 15 seconds, will be as next:

  • For linear: work start time + (15 * run attempt count)
  • For exponential: work start time + Math.scalb(15, run attempt count — 1)

The work start time, is when the work was first executed (the 1st run attempt).

Run attempt count is how many times the WorkManager has tried to execute an specific Work.

Also note that the maximum delay will be capped at WorkRequest.MAX_BACKOFF_MILLIS and take into consideration that a retry will only happen if returning WorkerResult.RETRY


// add initial delay only for OneTimeWorkRequest
val syncOnlyOnce = OneTimeWorkRequestBuilder<SyncWorker>()
.setInitialDelay(15, TimeUnit.MINUTES)
.build()
// backoff delay and policy
val syncOnlyOnce = OneTimeWorkRequestBuilder<SyncWorker>()
.setBackoffCriteria(BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MICROSECONDS)
.build()

Deloitte - Workout your tasks with WorkManager (2)

4️⃣ Chaining work

Sometimes it is necessary to run some tasks in parallel or to chain them one after another one, or even to create groups of tasks chained or in parallel. These features are available also in WorkManager library.

The code for the previous scheme looks like this:


WorkManager.getInstance(context)
.beginWith(task1)
.then(task2)
.then(task3)
.enqueue()

Parallel execution for Task 1 and Task 2


val leftChain = WorkManager.getInstance(context)
.beginWith(task1)
.then(task2)
val rightChain = WorkManager.getInstance(context)
.beginWith(task3)
.then(task4)
val resultChain = WorkContinuation
.combine(listOf(leftChain, rightChain))
.then(task5)
resultChain.enqueue()

Chained tasks and parallel chains


val leftChain = WorkManager.getInstance()
.beginWith(task1)
.then(task2)
val rightChain = WorkManager.getInstance()
.beginWith(task3)
.then(task4)
val resultChain = WorkContinuation
.combine(listOf(leftChain, rightChain))
.then(task5)
resultChain.enqueue()

5️⃣ Merge inputs and outputs

Like we already saw when we chain the work the outputs are inputs of the tasks, but they should be merged somehow in order to get the correct data. To do the merge we have 2 main strategies in place that are actually the provided implementations of the abstract class InputMerger:

6️⃣ Threading options in WorkManager

  1. ListenableWorker
  2. Worker
  3. CoroutineWorker
  4. RxWorker
  5. Our own implementation 🙂

🧵 ListenableWorker

Overview

  • A ListenableWorker only signals when the work should start and stop
  • The start work signal is invoked on the main thread, so we go to a background thread of our choice manually
  • A ListenableFuture is a lightweight interface: it is a Future that provides functionality for attaching listeners and propagating exceptions

Stop work

  • It is always cancelled when the work is expected to stop. Use a CallbackToFutureAdapter to add a cancellation listener

🧵 Worker

Overview

  • Worker.doWork() is called on a background thread, synchronously
  • The background thread comes from the Executor specified in WorkManager’s Configuration, but it could also be customised

Stop work

  • Worker.onStopped() is called. This method could be overridden or we could call Worker.isStopped() to checkpoint the code and free up resources when necessary

🧵 CoroutineWorker

Overview

  • For Kotlin users, WorkManager provides first-class support for coroutines
  • Instead of extending Worker, we should extend CoroutineWorker
  • CoroutineWorker.doWork() is a suspending function
  • The code runs on Dispatchers.Default, not on Executor (customisation by using CoroutineContext)

Stop work

  • CoroutineWorkers handle stoppages automatically by cancelling the coroutine and propagating the cancellation signals

🧵 RxWorker

Overview

  • For RxJava2 users, WorkManager provides interoperability
  • Instead of extending Worker, we should extend RxWorker
  • RxWorker.createWork() method returns a Single<Result> indicating the Result of the execution, and it is called on the main thread, but the return value is subscribed on a background thread by default. Override RxWorker.getBackgroundScheduler() to change the subscribing thread.

Stop work

  • Done by default

🎉WorkManager — Recap

  • WorkManager is a wrapper for the existing background processing solutions
  • Create one time or periodic work requests
  • Identify our tasks by using ids, tags and unique names
  • Add constraint, delay and retry policy
  • Use input/output data and merge them
  • Create chains of tasks
  • Use the available threading options or create your own

That’s all folks! 🐰 Enjoy and feel free to leave a comment if something is not clear or if you have questions. And if you like it please share !

Thank you for reading! 🙌🙏😍✌

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s