WorkManager (Android Architecture Component), Background Task + Kotlin

Nikhil Singh
Mobcoder LLC
Published in
10 min readDec 18, 2020

--

Hi Guys,

To start with the WorkManger directly first we should scratch some knowledge about Background Task.

So, Let's start with this !!

Definition of Background Task:

An app is considered to be running in the background as long as each of the following conditions is satisfied:

  1. None of the app’s activities are currently visible to the user.
  2. The app isn’t running any foreground services that started while activity from the app was visible to the user.

The following list shows common pending tasks that an app manages while it runs in the background:

  • Your app registers a broadcast receiver in the manifest file.
  • Your app schedules a repeating alarm using the Alarm Manager.
  • Your app schedules a background task, either as a worker using Work-Manager or a job using Job Scheduler.

Categories of background tasks:

Background tasks fall into the following main categories:

  • Immediate
  • Exact
  • Deferred

Immediate:

If the task needs to complete while the user is interacting with the application then this task should be categorized for immediate execution.

Example: Kotlin Coroutines for Kotlin and Threading for JAVA

Exact:

If the task needs to run at an exact time then categorize the task as exact.

Example: Alarm Manager

Deferred:

Most tasks don’t need to be run at an exact time. Tasks generally allow for slight variations in when they run that are based on conditions such as network availability and remaining battery. Tasks that don’t need to be run at an exact time should be categorized as deferred.

This decision tree helps you decide which category is best for your background task.
Fig 1: This decision tree helps you decide which category is best for your background task.

What is WorkManager?

WorkManager is an API that makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or the device restarts. The WorkManager API is a suitable and recommended replacement for all previous Android background scheduling APIs, including FirebaseJobDispatcher, GcmNetworkManager, and Job Scheduler.

Under the hood WorkManager uses an underlying job dispatching service based on the following criteria:

Fig 2: WorkManger Process

Why WorkManager?

Since Android Marshmallow, the Android team has started to put more focus on battery optimizations. The team introduced Doze mode and, later in Android Oreo, it introduced background service limits — just to mention a few. With Android Pie, the team continues to give attention to optimization and introduce new changes.

Until now, to perform background work, developers not only had to know about these battery optimizations but also had to choose between the many ways to implement the work.

Internally, WorkManager uses the existing mentioned options to perform background work: JobScheduler, Firebase JobDispatcher, and Alarm Manager + Broadcast receivers.

Depending on the configuration of a given device that your app is installed on (OS version, Google Play services availability, etc.), WorkManager will choose an appropriate option and execute the work when it considers the optimum time to do so.

In doing so, WorkManager provides a simple and clean interface and “hides” the complexity of deferrable but guaranteed background work from developers.

Key Points of WorkManager:

Work Constraints

Declaratively define the optimal conditions for your work to run using Work Constraints. (For example, run only when the device is Wi-Fi, when the device idle, or when it has sufficient storage space, etc.)

Robust Scheduling

WorkManager allows you to schedule work to run one- time or repeatedly using flexible scheduling windows. Work can be tagged and named as well, allowing you to schedule unique, replaceable work and monitor or cancel groups of work together. Scheduled work is stored in an internally managed SQLite database and WorkManager takes care of ensuring that this work persists and is rescheduled across device reboots. In addition, WorkManager adheres to power-saving features and best practices like Doze mode, so you don’t have to worry about it.

Flexible Retry Policy

Sometimes work fails. WorkManager offers flexible retry policies, including a configurable exponential backoff policy.

Work Chaining

For complex related work, chain individual work tasks together using a fluent, natural, interface that allows you to control which pieces run sequentially and which run in parallel.

What is Work?

Work is defined using the Worker class. The doWork() method is run synchronously on a background thread provided by WorkManager.

To create some work for WorkManager to run, extend the Worker class, and override the doWork() method.

What is WorkRequest?

Work is defined in WorkManager via a WorkRequest. In order to schedule any work with WorkManager, you must first create a WorkRequest object and then enqueue it.

The WorkRequest object contains all of the information needed by WorkManager to schedule and run your work. It includes constraints that must be met for your work to run, scheduling information such as delays or repeating intervals, retry configuration, and may include input data if your work relies on it.

There are two derived implementations of this class that you can use to create the request.

  • OneTimeWorkRequest: OneTimeWorkRequest is useful for scheduling non-repeating work.
  • PeriodicWorkRequest: PeriodicWorkRequest is more appropriate for scheduling work that repeats on some interval.

So Friends, It was just basically about the WorkManger Now We move to implementation with a simple demo project.

First, add the following dependencies to your app’s build.gradle file and sync your project.

def work_version = "2.4.0"// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"

After that, We will create a Worker class to perform a work namely NotificationWorker that extends the Worker class and override the doWork() method.

The Result returned from doWork() informs the WorkManager service whether the work succeeded and, in the case of failure, whether or not the work should be retried.

  • Result.success(): The work finished successfully.
  • Result.failure(): The work failed.
  • Result.retry(): The work failed and should be tried at another time according to its retry policy.

Here in the doWork method, We are calling displayNotificaton() method that shows the notification.

Now we move to Main Activity where we enqueue the WorkRequest.

Above in our MainActivity we are creating a workRequest using OneTimeWorkRequest. And after a click on the Start Work button, we start this Notification Display task using an enqueue method by passing the mWorkRequest object we created.

Here we are also observing the WorkInof by its states But before observing it Let’s first understand the States cycle of WorkInfo.

Work States:

Work goes through a series of State changes over its lifetime.

  • One-time work states:
    For a one-time work request, your work begins in an ENQUEUED state.

In the ENQUEUED state, your work is eligible to run as soon as its Constraints and initial delay timing requirements are met. From there it moves to a RUNNING state and then depending on the outcome of the work it may move to SUCCEEDED, FAILED, or possibly back to ENQUEUED if the result is retry. At any point in the process, work can be canceled, at which point it will move to the CANCELLED state.

Figure 3 illustrates the life of one-time work, with the events that may take it to another state.

figure 3: State diagram of one-time work.

SUCCEEDED, FAILED and CANCELLED all represent a terminal state for this work. If your work is in any of these states, WorkInfo.State.isFinished() returns true.

  • Periodic Work States:

Success and failed states apply only to one-time and chained work. For periodic work, there is only one terminal state, CANCELLED. This is because periodic work never ends. After each run, it’s rescheduled, regardless of the result. Figure 4 depicts the condensed state diagram for periodic work.

figure 4: State diagram of Periodic work

Now come to the MainActivity again and you will here, observing the WorkInfo states and shows that using Toast message.

It will show the message as per the OneTimeWork states i.e. Eqnueued, Running, Success then Finished.

Canceling a work:

If you no longer need your previously enqueued work to run, you can ask for it to be canceled. Work can be canceled by its name, id, or by a tag associated with it.

Under the hood, WorkManager checks the State of the work. If the work is already finished, nothing happens. Otherwise, the work’s state is changed to CANCELLED and the work will not run in the future. Any WorkRequest jobs that are dependent on this work will also be CANCELLED.

Currently RUNNING work receives a call to ListenableWorker.onStopped(). Override this method to handle any potential cleanup.

Here ListenableWorker is a superclass of Worker class that we extended in our NoitificationWorker class.

Stop a Running Worker:

There are a few different reasons your running Worker might be stopped by WorkManager:

  • You explicitly asked for it to be canceled (by calling WorkManager.cancelWorkById(UUID), for example).
  • In the case of unique work, you explicitly enqueued a new WorkRequest with an ExistingWorkPolicy of REPLACE. The old WorkRequest is immediately considered canceled.
  • Your work’s constraints are no longer met.
  • The system instructed your app to stop your work for some reason. This can happen if you exceed the execution deadline of 10 minutes. The work is scheduled for retry at a later time.

Under these above conditions, your Worker is stopped.

onStopped() callback:
WorkManager invokes ListenableWorker.onStopped() as soon as your Worker has been stopped. Override this method to close any resources you may be holding onto.

isStopped() property:
You can call the ListenableWorker.isStopped() method to check if your worker has already been stopped. If you're performing long-running or repetitive operations in your Worker, you should check this property frequently and use it as a signal for stopping work as soon as possible.

Assigning Input Data:

Your work may require input data in order to do its work. For example, work that handles uploading an image might require the URI of the image to be uploaded as input.

Input values are stored as key-value pairs in a Data object and can be set on the work request. WorkManager will deliver the input Data to your work when it executes the work. The Worker class can access the input arguments by calling Worker.getInputData(). The code below shows how you can create a Worker instance that requires input data and how to send it in your work request.

Tag work:

Every work request has a unique identifier, which can be used to identify that work later in order to cancel the work or observe its progress.

If you have a group of logically related work, you may also find it helpful to tag those work items. Tagging allows you to operate with a group of work requests together.

For example, WorkManager.cancelAllWorkByTag(String) cancels all Work Requests with a particular tag, and WorkManager.getWorkInfosByTag(String) returns a list of the WorkInfo objects which can be used to determine the current work state.

As you can see above the example we have been tagged namely “Notification” to our OneTimeWorkRequest.

Retry and Backoff Policy:

If you require that WorkManager retry your work, you can return Result.retry() from your worker. Your work is then rescheduled according to a backoff delay and backoff policy.

Backoff delay specifies the minimum amount of time to wait before retrying your work after the first attempt. This value can be no less than 10 seconds (or MIN_BACKOFF_MILLIS).

Backoff policy defines how the backoff delay should increase over time for subsequent retry attempts. WorkManager supports two backoff policies, LINEAR and EXPONENTIAL.

Every work request has a backoff policy and backoff delay. The default policy is EXPONENTIAL with a delay of 10 seconds, but you can override this in your work request configuration.

Here is an example of customizing the backoff delay and policy.

In this example, the minimum backoff delay is set to the minimum allowed value, 10 seconds. Since the policy is LINEAR the retry interval will increase by approximately 10 seconds with each new attempt. For instance, the first run finishing with Result.retry() will be attempted again after 10 seconds, followed by 20, 30, 40, and so on, if the work continues to return Result.retry() after subsequent attempts. If the backoff policy were set to EXPONENTIAL, the retry duration sequence would be closer to 20, 40, 80, and so on.

Work constraints:

Constraints ensure that work is deferred until optimal conditions are met. The following constraints are available to WorkManager.

  • NetworkType Constrains the type of network required for your work to run. For example, Wi-Fi (UNMETERED).
  • BatteryNotLow When set to true, your work will not run if the device is in low battery mode.
  • RequiresCharging When set to true, your work will only run when the device is charging.
  • DeviceIdle When set to true, this requires the user’s device to be idle before the work will run. This can be useful for running batched operations that might otherwise have a negative performance impact on other apps running actively on the user’s device.
  • StorageNotLow When set to true, your work will not run if the user’s storage space on the device is too low.

To create a set of constraints and associate it with some work, create a Constraint instance using the Constraints.Builder() and assign it to your WorkRequest.Builder().

For example, the following code builds a work request which only runs when the user’s device is charging.

We have done with the simple demo application using WorkManger. I hope, you are now ready to use the WorkManger for background tasks in your project.

For the complete code, you can download or check from the GitHub link.

--

--