Use of WorkManager in Android App (Part-3)

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

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

1. Identify a task

2. Get the status of a task

3.WorkContinuation for sequential and concurrent execution

4.WorkManager Policies

5. Setting Input and Output Data for a Worker

6.Threading options in WorkManager

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

By UUID

UUID syncWorkId = syncOnlyOnceWork.getId();

By Tagging work

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

By Unique work sequence

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

By Unique work chain

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

2. Get the status of a task

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()
.getWorkInfoByIdLiveData(syncOnlyOnce.id)
.observe(this, Observer {
workInfo ->
if (workInfo != null && workInfo.state == State.SUCCEEDED)
{
displayMessage("Sync finished!")
}
})
//by tagWorkManager.getInstance()
.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 nameWorkManager.getInstance()
.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

Cancel a task based on its identity

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

3. 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:

4. WorkManager Policies

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.

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 a 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 OneTimeWorkRequestval syncOnlyOnce = OneTimeWorkRequestBuilder<SyncWorker>()   
.setInitialDelay(15, TimeUnit.MINUTES)
.build() // backoff delay and policyval
syncOnlyOnce = OneTimeWorkRequestBuilder<SyncWorker>()
.setBackoffCriteria(BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MICROSECONDS)
.build()

5. 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

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

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.

For using above InputMerger you can refer Code from this code link

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 canceled 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 customized

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 (customization by using CoroutineContext)

Stop work

  • CoroutineWorkers handle stoppages automatically by canceling 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

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 serialized form.

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

🎉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 👏 and share!

Thank you for reading! 🙌🙏😍✌

--

--

--

Senior Software Engineer | Android | Java | Kotlin|Xamarin Native Android|Flutter

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Keep Your Regular Expression Escapes in Android

Simple Sharing Color Theming for XML and Jetpack Compose

Java and Android Threads Tutorial for Beginners

More Magical Android Dagger 2

The Imperfect ‘Send Email’ Action in Android

How to Add Manifest Permission to an Application

Correlating Jetpack Compose Side Effects

Coding-free Integration of AppGallery Connect Crash in Flutter

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Abhishek Srivastava

Abhishek Srivastava

Senior Software Engineer | Android | Java | Kotlin|Xamarin Native Android|Flutter

More from Medium

Basics for your first Android App: REST API, Database, and Fragment Navigation

How to get Darker shadows on Android

Activity Launch Mode

Android 13: Notification Runtime Permission