I. Overview of WorkManager

1.1 the WorkManager profile

In Android application development, there are more or less requirements for background tasks. According to different demand scenarios, Android provides a variety of solutions for background tasks, such as Service, Loader, JobScheduler and AlarmManger. Background tasks are usually used for functions that do not require user awareness, and they need to be closed immediately after the execution of background tasks to recover resources. If these apis are not properly used, a large amount of electricity will be consumed. In order to solve the problem of high battery consumption on Android, Android officials have made various optimizations, from Doze to App Standby, by adding various limits and managing application processes to wrap applications without high battery consumption.

To solve Android’s power consumption problem, Android provides WorkManager, which provides a unified solution for tasks in applications that don’t need to be completed in a timely manner. Developers can easily schedule deferred asynchronous tasks that should run even when they exit the application or restart the device. WorkManager is an AP that replaces components such as the previous Android background scheduling apis (FirebaseJobDispatcher, GcmNetworkManager and JobScheduler). WorkManager requires API level 14 and battery life.

The compatibility of WorkManager depends on the system version. When the API is lower than 23, AlarmManager+Broadcast Receiver is used; when the API is higher than 23, JobScheduler is used. Either way, the task is ultimately left to the Executor. The following figure shows the flow of the underlying job scheduling service of WorkManager.It is important to note that WorkManager is not a new worker thread, and it has not emerged to replace other types of worker threads. Whereas a worker thread is usually able to execute immediately and report the results back to the user when the task is complete, WorkManager is not real-time and does not guarantee that the task will be executed immediately.

1.2 the WorkManager characteristics

WorkManager has the following three features:

  • It is used to achieve tasks that do not need to be completed immediately, such as background downloading of open screen advertisements, uploading log information, etc.
  • To ensure that the task will be carried out;
  • Strong compatibility.

For tasks that don’t need to be completed immediately

In Android development, often encounter background download, upload log information and other requirements, generally speaking, these tasks do not need to be completed immediately, if we use to manage these tasks, the logic may be very responsible, and if handled improperly will cause a lot of power consumption.

Background delay task

WorkManager can guarantee that a task will be executed, but it is not impossible to guarantee that it will be executed immediately, i.e., at an appropriate time. Because WorkManager has its own database, task-related information and data are stored in the database. Therefore, as long as the task is submitted to WorkManager, there is no need to worry about losing the task even if the application is rolled out or the device is restarted.

Wide compatibility

WorkManager is compatible with API 14 and doesn’t require your device to have Google Play Services installed, so don’t worry about compatibility issues.

In addition, WorkManager has a number of other key advantages.

Work constraints

Use work constraints to clearly define the best conditions under which work can run. For example, run only when the device is connected to a Wi-Fi network, is idle, or has sufficient storage space.

Powerful scheduling

WorkManager allows developers to schedule work using flexible scheduling Windows to run one-off or repetitive work. Work can also be tagged or named to schedule unique, replaceable work and to monitor or cancel workgroups. The scheduled work is stored in an internally hosted SQLite database, and the WorkManager is responsible for ensuring that the work continues and is rescheduled when the device is restarted. In addition, WorkManager follows power saving features and best practices such as low power consumption mode, so developers don’t have to worry about power consumption.

Flexible retry policy

Sometimes tasks fail, and WorkManager provides flexible retry policies, including a configurable exponential backoff policy.

Work link

For complex, related work, we can use a smooth, natural interface to link work tasks together so that we can control which parts run in sequence and which parts run in parallel, as shown below.

WorkManager.getInstance(...). .beginWith(Arrays.asList(workA, workB))
    .then(workC)
    .enqueue(a);Copy the code

Built-in thread interoperability

WorkManager seamlessly integrates RxJava and coroutines, giving you the flexibility to plug in your own asynchronous apis.

1.3 Concepts of WorkManager

There are several important concepts to note when working with WorkManager.

  • Worker: the performer of a task, an abstract class that needs to be inherited to implement the task to be executed.
  • WorkRequest: Specifies which Woker should perform the task, the environment in which it should be executed, the order in which it should be executed, and so on. Use its subclasses OneTimeWorkRequest or PeriodicWorkRequest.
  • WorkManager: Manages task requests and task queues. The initiated WorkRequest will enter its task queue.
  • WorkStatus: Contains the status of the task and information about the task in the form of LiveData for the observer.

Two, basic use

2.1 Adding a Dependency

To get started with WorkManager, import the library into your Android project.

dependencies {
  def work_version = "2.4.0"
  implementation "androidx.work:work-runtime:$work_version"
}
Copy the code

After adding dependencies and synchronizing Gradle projects.

2.2 define the Worker

Create a Worker class that inherits from Worker, and then execute the task to be run in the Worker class’s doWork() method, and return the result of the task state. For example, the task of uploading an image is implemented in the doWork() method.

public class UploadWorker extends Worker {
   public UploadWorker( @NonNull Context context, @NonNull WorkerParameters params) {
       super(context, params);
   }

   @Override
   public Result doWork(a) {
     // Do the work here--in this case, upload the images.
     uploadImages(a);return Result.success();
   }
}
Copy the code

Tasks performed in the doWork() method eventually return an object of type Result, representing the Result of the task execution, with three enumerated values.

  • Result.success() : Work completed successfully.
  • Result.failure() : Failure to work.
  • Result.retry() : The work fails and is tried at another time according to its retry policy.

2.3 create WorkRequest

After the definition of Worker is completed, the WorkManager service must be used to schedule the work before it can run. WorkManager provides a great deal of flexibility in how work is scheduled. Developers can schedule it to run regularly over a certain period of time, or they can schedule it to run only once.

Regardless of how you choose to schedule work, use WorkRequest to perform the request for the task. Worker defines the unit of work, and WorkRequest (and its subclasses) defines how and when the work runs, as shown below.

WorkRequest uploadWorkRequest =
   new OneTimeWorkRequest.Builder(UploadWorker.class)
       .build(a);Copy the code

The WorkRequest is then submitted to the WorkManager using the WorkManager’s enqueue() method, as shown below.

WorkManager
    .getInstance(myContext)
    .enqueue(uploadWorkRequest);
Copy the code

The exact time to execute the worker depends on the constraints used in WorkRequest and how the system is optimized.

Method guide

3.1 WorkRequest

3.1.1 WorkRequest overview

WorkRequest is mainly used to submit task requests to workers. We can use WorkRequest to deal with the following common scenarios.

  • Dispatch one-time and repetitive work
  • Set work constraints, such as a requirement to be connected to a Wi-Fi network or to be charging before WorkRequest is executed
  • Make sure to delay work for at least a certain amount of time
  • Set retry and retreat policies
  • Pass the input data to the job
  • Use tags to group related work together

WorkRequest is an abstract class with two subclasses, OneTimeWorkRequest, which implements once-only tasks, and PeriodicWorkRequest, which implements periodic tasks.

3.1.2 One-off Task

If the task only needs to be executed once, you can use a subclass of WorkRequest, OneTimeWorkRequest. For simple work that requires no additional configuration, you can use the static method from() of the OneTimeWorkRequest class, as shown below.

WorkRequest myWorkRequest = OneTimeWorkRequest.from(MyWork.class);
Copy the code

For more complex work, you can create the WorkRequest in a builder fashion, as shown below.

WorkRequest uploadWorkRequest =
   new OneTimeWorkRequest.Builder(MyWork.class)
       .build(a);Copy the code

3.1.3 Periodic tasks

If you need to run some work on a regular basis, you can use PeriodicWorkRequest. For example, you may need to periodically back up data, periodically download fresh application content, or periodically upload logs to the server.

PeriodicWorkRequest saveRequest =
       new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class, 1, TimeUnit.HOURS)
           .build(a);Copy the code

The above code defines a periodic task that runs at intervals of one hour. However, the exact execution time of the worker depends on the constraints you set in the WorkRequest object and the optimizations performed by the system.

If the nature of the task is time-sensitive, PeriodicWorkRequest can be configured to run within flexible periods of each interval, as shown in Figure 1.

To define periodic work with flexible time periods, pass flexInterval and repeatInterval parameters when creating PeriodicWorkRequest, as shown below.

WorkRequest saveRequest =
       new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class,
               1, TimeUnit.HOURS,
               15, TimeUnit.MINUTES)
           .build(a);Copy the code

The code above is meant to run regular work in the last 15 minutes of each hour.

3.1.4 Work constraints

To make work run in a given environment, we can add constraints to WorkRequest. Common constraints are shown below.

  • NetworkType: specifies the NetworkType required for running work, for example, wi-fi (UNMETERED).
  • BatteryNotLow: If set to true, the device does not work when it is in “low battery mode”.
  • RequiresCharging: If set to true, work can only run while the device is charging.
  • DeviceIdle: If this parameter is set to true, the user’s device must be idle.
  • StorageNotLow: If set to true, work will not run when there is insufficient storage space on the user’s device.

For example, the following code builds a work request that runs only when the user’s device is charging and connected to a Wi-Fi network.

Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED)
       .setRequiresCharging(true).build(a); WorkRequest myWorkRequest =new OneTimeWorkRequest.Builder(MyWork.class)
               .setConstraints(constraints)
               .build(a);Copy the code

If a constraint is not met when the work runs, the WorkManager will stop working, and the system will retry the work after all the constraints are met.

3.1.5 Delay of work

If the work has no constraints and all constraints are satisfied, the system may choose to run the work immediately when it is enqueued. If you do not want work to run immediately, you can specify that work should start after a minimum initial delay.

WorkRequest myWorkRequest =
      new OneTimeWorkRequest.Builder(MyWork.class)
               .setInitialDelay(10, TimeUnit.MINUTES)
               .build(a);Copy the code

The above code sets the task to run at least 10 minutes after it has been queued.

3.1.6 Retry and Retreat policy

If the WorkManager needs to retry the work, the worker returns result.retry (), and the system reschedules the work based on the backout delay time and backout policy.

  • Backout delay Specifies the minimum time to wait before retrying work after the first attempt, generally no more than 10 seconds (or MIN_BACKOFF_MILLIS).
  • The fallback policy defines how the fallback delay increases over time during subsequent retries. WorkManager supports two exit policies, LINEAR and EXPONENTIAL.

Each work request has an exit policy and an exit delay. The default policy is EXPONENTIAL, with a delay of 10 seconds, which can be replaced by the developer in the work request configuration.

WorkRequest myWorkRequest =
       new OneTimeWorkRequest.Builder(MyWork.class)
               .setBackoffCriteria(
                       BackoffPolicy.LINEAR,
                       OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                       TimeUnit.MILLISECONDS)
               .build(a);Copy the code

3.1.7 tag WorkRequest

Each work request has a unique identifier that can be used to identify the work so that it can be canceled or its progress can be observed. If you have a group of logically related work, it might also be helpful to tag those work items. The tag is added to the WorkRequest using the addTag() method, as shown below.

WorkRequest myWorkRequest =
       new OneTimeWorkRequest.Builder(MyWork.class)
       .addTag("cleanup").build(a);Copy the code

Finally, you can add multiple tags to a single work request, which are stored internally as a set of strings. For a WorkRequest, we can retrieve its tag set through workrequest.gettags ().

3.1.8 Assigning Input Data

Sometimes tasks require input data to function properly. For example, when an image upload task requires the URI of the uploaded image as the input data, this scenario is called allocating input data.

Input values are stored in a Data object as key-value pairs and can be set in a work request. The WorkManager passes the input Data to the Worker when the work is performed, and the Worker class can access the input parameters by calling worker.getinputData (), as shown below.

public class UploadWork extends Worker {

   public UploadWork(Context appContext, WorkerParameters workerParams) {
       super(appContext, workerParams);
   }

   @NonNull
   @Override
   public Result doWork(a) {
       String imageUriInput = getInputData().getString("IMAGE_URI");
       if(imageUriInput == null) {
           return Result.failure(a); }uploadFile(imageUriInput);
       return Result.success();
   }
   ...
}

// Create a WorkRequest for your Worker and sending it input
WorkRequest myUploadWork =
      new OneTimeWorkRequest.Builder(UploadWork.class)
           .setInputData(
               new Data.Builder().putString("IMAGE_URI"."http://...").build()).build(a);Copy the code

The code above shows how to create a Worker instance that requires input data and how to send it in a work request.

3.2 the Work state

Work goes through a series of State changes throughout its life cycle, with State changes divided into one-time task State and periodic task State.

3.2.1 One-off Task status

For one-off task requests, the initial state of the work isENQUEUED. inENQUEUEDIn state, the task runs as soon as its Constraints and initial delay timing requirements are satisfied. Next, the job turns toRUNNINGStatus, which may then be converted depending on the results of the workSUCCEEDED,FAILEDState; Or, if the result is retry, it might come backENQUEUEDState. During this process, work can be cancelled at any time, and the work will enter after cancellationCANCELLEDState.The figure above shows the life-cycle status of a one-time job, with SUCCEEDED, FAILED and CANCELLED indicating the terminated state of the job. If your job is in any of these states,WorkInfo.State.isFinished()Both will return true.

3.2.2 Periodic Task Status

Success and failure states only apply to one-off tasks and chain jobs, while regular jobs have only one terminating state CANCELLED because they never end. After each run, the system reschedules it, regardless of the result.

The figure above shows how the life cycle status of a scheduled task changes.

3.3 Task Management

3.3.1 Unique Task

After defining Worker and WorkRequest, the last step is to queue the work. The easiest way to queue the work is to call the WorkManager enqueue() method and pass the WorkRequest to run. In order to achieve this goal, we can schedule the work as a unique task.

Unique tasks ensure that there is only one working instance with a specific name at a time. Unlike system-generated ids, unique names are specified by the developer rather than automatically generated by the WorkManager. Unique tasks can be used for either one-time or periodic tasks. Depending on whether you schedule repetitive or one-time tasks, you can create a unique sequence of tasks by calling one of the following methods.

  • The WorkManager. EnqueueUniqueWork () : used for one-time work
  • The WorkManager. EnqueueUniquePeriodicWork () : used to work on a regular basis

Also, both methods take three arguments.

  • NiqueWorkName: String used to uniquely identify the work request.
  • ExistingWorkPolicy: This enum tells the WorkManager what action to perform if there is already a unique work chain with this name that has not yet completed. For more information, see conflict Resolution Policy.
  • Work: Indicates the WorkRequest to be scheduled.

Here is the code for solving the duplicate scheduling problem using unique tasks.

PeriodicWorkRequest sendLogsWorkRequest = new
      PeriodicWorkRequest.Builder(SendLogsWorker.class, 24, TimeUnit.HOURS)
              .setConstraints(new Constraints.Builder().setRequiresCharging(true).build()).build(a); WorkManager.getInstance(this).enqueueUniquePeriodicWork(
     "sendLogs",
     ExistingPeriodicWorkPolicy.KEEP,
     sendLogsWorkRequest);
Copy the code

If the sendLogs job is already in a queue, the system keeps the existing job and does not add new jobs.

3.3.2 Conflict resolution policy

Sometimes the scheduling of tasks conflicts, and we need to tell the WorkManager what to do in the event of a conflict, which we can do by passing an enumeration when the work is queued. For one-off tasks, the system provides an ExistingWorkPolicy enumeration, which supports the following options for handling conflicts.

  • REPLACE: REPLACE an existing job with a new one. This option cancels existing work.
  • KEEP your current job and ignore the new one.
  • APPEND: Appends the new work to the end of the existing work. This policy will cause your new work to link to your existing work and run after the existing work is finished.

Existing jobs will become a prerequisite for new jobs, which will also be CANCELLED or FAILED if existing jobs become CANCELLED or FAILED. If you want to run the new work regardless of the state of the existing work, use APPEND_OR_REPLACE. APPEND_OR_REPLACE runs regardless of whether the state becomes CANCELLED or FAILED.

3.4 Observing the Task Status

After a task has been queued, we can query information about the task in the WorkManager based on its name, ID, or tag associated with it, and check its status in the following ways.

// by id
workManager.getWorkInfoById(syncWorker.id); // ListenableFuture<WorkInfo>

// by name
workManager.getWorkInfosForUniqueWork("sync"); // ListenableFuture<List<WorkInfo>>

// by tag
workManager.getWorkInfosByTag("syncTag"); // ListenableFuture<List<WorkInfo>>
Copy the code

The query returns the ListenableFuture of the WorkInfo object, containing the id of the work, its flag, its current State, and any outputData set by result.success (outputData). With each method’s LiveData, we can observe changes to WorkInfo by registering listeners, as shown below.

workManager.getWorkInfoByIdLiveData(syncWorker.id)
        .observe(getViewLifecycleOwner(), workInfo -> {
    if (workInfo.getState() != null &&
            workInfo.getState() == WorkInfo.State.SUCCEEDED) {
        Snackbar.make(requireView(),
                    R.string.work_completed, Snackbar.LENGTH_SHORT)
                .show();
   }
});
Copy the code

Also, WorkManager 2.4.0 and later supports complex queries against queued jobs using WorkQuery objects, which support queries by a combination of job tags, status, and unique job names, as shown below.

WorkQuery workQuery = WorkQuery.Builder
       .fromTags(Arrays.asList("syncTag")).addStates(Arrays.asList(WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
       .addUniqueWorkNames(Arrays.asList("preProcess"."sync")).build(a); ListenableFuture<List<WorkInfo>> workInfos = workManager.getWorkInfos(workQuery);
Copy the code

The above code looks for all tasks marked “syncTag”, FAILED or CANCELLED, and with a unique work name of “preProcess” or “sync”.

3.5 Canceling or Stopping a Task

3.5.1 Canceling a Task

WorkManager supports canceling tasks in a pair column by name, ID, or tag associated with the job, as shown below.

// by id
workManager.cancelWorkById(syncWorker.id);

// by name
workManager.cancelUniqueWork("sync");

// by tag
workManager.cancelAllWorkByTag("syncTag");
Copy the code

The WorkManager checks the current State of the task in the background. If the work is complete, the system does not perform any operation. Otherwise the status of the job will change to CANCELLED and the job will not run.

3.5.2 Stopping a Task

A running task may stop running for a number of reasons, including the following.

  • Clearly want to cancel it, can call the WorkManager. CancelWorkById (UUID) method.
  • If it is the only task, when a new WorkRequest whose ExistingWorkPolicy is REPLACE is added to the queue, the old WorkRequest is immediately considered cancelled.
  • The added task constraints no longer fit.
  • The system tells the application to stop working for some reason.

When the task is stopped, the WorkManager will immediately call ListenableWorker. OnStopped () close may retain all of the resources.

3.6 Observing the task progress

WorkManager 2.3.0 provides support for setting and observing the intermediate progress of tasks. If the application is running in the foreground and the worker is still running, you can also use WorkInfo’s LiveData Api to show this information to the user. ListenableWorker supports the use of the setProgressAsync() method to preserve intermediate progress. ListenableWorker can only observe and update progress information at run time.

3.6.1 Updating the progress

For Java developers, we can use ListenableWorker or Worker’s setProgressAsync() method to update the progress of an asynchronous process. For developers with lower ears than Kotlin, progress information can be updated using the setProgress() extension function of the CoroutineWorker object. , as shown below. Java write:

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class ProgressWorker extends Worker {

    private static final String PROGRESS = "PROGRESS";
    private static final long DELAY = 1000L;

    public ProgressWorker( @NonNull Context context, @NonNull WorkerParameters parameters) {
        super(context, parameters);
        // Set initial progress to 0
        setProgressAsync(new Data.Builder().putInt(PROGRESS, 0).build());
    }

    @NonNull
    @Override
    public Result doWork(a) {
        try {
            // Doing work.
            Thread.sleep(DELAY);
        } catch (InterruptedException exception) {
            // ... handle exception
        }
        // Set progress to 100 after you are done doing your work.
        setProgressAsync(new Data.Builder().putInt(PROGRESS, 100).build());
        return Result.success();
    }
}
Copy the code

Kotlin writing:

import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import kotlinx.coroutines.delay

class ProgressWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {

    companion object {
        const val Progress = "Progress"
        private const val delayDuration = 1L
    }

    override suspend fun doWork(): Result {
        val firstUpdate = workDataOf(Progress to 0)
        val lastUpdate = workDataOf(Progress to 100)
        setProgress(firstUpdate)
        delay(delayDuration)
        setProgress(lastUpdate)
        return Result.success()}}Copy the code

3.6.2 Observing the progress

To observe progress, use getWorkInfoBy… () or getWorkInfoBy… The LiveData() method, which returns WorkInfo, as shown below.

WorkManager.getInstance(getApplicationContext())
     // requestId is the WorkRequest id
     .getWorkInfoByIdLiveData(requestId)
     .observe(lifecycleOwner, new Observer<WorkInfo>() {
             @Override
             public void onChanged(@Nullable WorkInfo workInfo) {
                 if(workInfo ! = null) { Data progress = workInfo.getProgress(a);int value = progress.getInt(PROGRESS, 0)
                     // Do something with progress}}});Copy the code

Reference:

Android Navigation Jetpack (4) LiveData Android Lifecycle Android Jetpack Lifecycle Android Jetpack Lifecycle Android Jetpack Lifecycle Android Jetpack