“This is the 12th day of my participation in the First Challenge 2022. For details: First Challenge 2022”

A, the WorkManager

WorkManager is the recommended solution for persistent work. Work is persistent when the schedule is maintained through application restarts and system restarts. Since most background processing is best done through persistence work, WorkManager is the primary recommended API for background processing.

Periodic tasks registered with WorkManager do not necessarily execute on time. This is not a Bug, but rather the system may try to reduce power consumption by putting together several tasks that trigger events close to each other. This can significantly reduce the number of CPU awakenings, thus effectively increasing battery life.

Note: WorkManager and Seriver are not a concept, nor are they directly related. Seriver is one of the four components of the Android system. WorkManager is just a tool for processing scheduled tasks.

1.1 Types of WorkManager

WorkManager handles three types of persistence work:

  • Immediate: A task that must be started immediately and completed soon. It may accelerate.

  • Long Running: Tasks may run for a longer time, perhaps more than 10 minutes.

  • Deferrable(Deferrable) : Scheduled tasks, to be started at a later time, can be run periodically.

The following diagram summarizes how the different types of persistence efforts relate to each other:

1.2 Provides an overview of the various types of work

Type Periodicity How to access
Immediate At a time OneTimeWorkRequestWorker

For urgent work, call it on OneTimeWorkRequestsetExpedited()
Long Running Once or regularly anyWorkRequest 和 Worker.

inWorkerIn the callsetForeground()Process notifications.
Deferrable Once or regularly PeriodicWorkRequest 和 Worker.

1.3 Reliable work with WorkManager

WorkManager is designed for work that needs to run reliably, even when the user leaves the screen, the application exits, or the device restarts. Such as:

  • Send logs or analysis to the back-end service.
  • Periodically synchronize application data with the server.

WorkManager is not intended for in-process background work that can be safely terminated when an application process disappears. Nor is it a one-size-fits-all solution for everything that needs to be done immediately.

1.4 Why use WorkManager?

WorkManager runs in the background and is best practice for addressing compatibility issues as well as battery and system health.

In addition, with WorkManager, you can schedule periodic tasks and complex chains of related tasks: background work can be executed in parallel or sequentially, and the order in which it is executed is specified. WorkManager seamlessly handles input and output passing between tasks.

You can also set standards for when to run background tasks. For example, if the device does not have a network connection, there is no reason to make an HTTP request to a remote server.

For example, we build a pipeline of concurrent tasks to apply filters to images. The results are then sent to the compression task, and then to the upload task.

1.5 Benefits of Using WorkManager

  • Handles compatibility with different operating system versions.
  • Follow system health best practices.
  • Supports asynchronous one-off and periodic tasks.
  • Support for chained tasks with input/output.
  • Allows you to set constraints on the running time of tasks.
  • Ensure that the task executes even if the application or device is restarted.

1.6 Working principle of WorkManager scheduler

To ensure compatibility with API level 14, WorkManager selects the appropriate way to schedule background tasks based on the device API level. The WorkManager may use JobScheduler or a combination of BroadcastReceiver and AlarmManager.

1.7 Replacing deprecated apis

The WorkManager API is a recommended alternative to all previous Android background scheduling apis, including FirebaseJobDispatcher, GcmNetworkManager, and Job Scheduler.

Note: If your application is for Android 10 (API level 29) or higher, Your FirebaseJobDispatcher and GcmNetworkManager API calls will no longer work on devices running Android Marshmallow (6.0) and later.

Basic usage of WorkManager

2.0 concept

2.0.1 The basic usage of WorkManager is simple in three steps.

  • 1. Define tasks and implement specific task logic.
  • 2. Configure the operating conditions and constraints of the task, and build the task request.
  • 3. Submit task requests to the system.

2.0.2 There are two ways to build a task request:

  • OneTimeWorkRequest: one-time Work request
  • PeriodicWorkRequest: PeriodicWorkRequest

2.0.3 Task Working status

Working status of one-time task:

Working status of periodic tasks:

2.0.4 Overview of core classes

  • Worker(used to define tasks) : Worker is an abstract class that specifies specific tasks to be performed. Use it by inheriting the class and implementing the doWork() method inside, where you write the concrete business logic.

  • WorkRequest: Represents a task request. A WorkRequest object must specify at least one Worker class. You can also add to the WorkRequest object, specifying the environment in which the task should run, and so on. Each person WorkRequest has an automatically generated unique ID that can be used to perform actions such as unqueuing a task or getting the status of a task. WorkRequest is an abstract class; There are two direct subclasses OneTimeWorkRequest and PeriodicWorkRequest. There are two classes associated with WorkerRequest:

    • WorkRequest. Builder: Used to create WorkRequest helper classes of objects, it has two subclasses OneTimeWorkRequest. Builder and PeriodicWorkRequest. Builder, correspond to create both the above two WorkerRequest.
    • Constraints: Specifies the runtime Constraints of the task (for example, “can only run when connected to a network”). This object can be created using Constraints.Builder and passed to WorkerRequest before calling the build() method of workRequest.Builder.
  • WorkManager(Submit Task Request) : This class is used to schedule and manage work requests. The order of the WorkRequest objects created earlier is arranged through the WorkManager. When scheduling tasks, WorkManager distributes system resources to perform operations such as load balancing, and complies with the preceding task constraints.

  • WorkInfo: This class contains the WorkRequest, which contains the ID of the WorkRequest, its current WorkStatus(including only SUCCEEDED and FAILED), output, label, and progress.

Is it very simple, let’s use Java and Kotlin two languages to achieve.

2.1 Adding a Dependency

Add the following dependencies to your app/build.gradle file:

dependencies {
    def work_version = "2.7.1"

    // (Java only)
    implementation "androidx.work:work-runtime:$work_version"

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

    // optional - RxJava2 support
    implementation "androidx.work:work-rxjava2:$work_version"

    // optional - GCMNetworkManager support
    implementation "androidx.work:work-gcm:$work_version"

    // optional - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"

    // optional - Multiprocess support
    implementation "androidx.work:work-multiprocess:$work_version"
}
Copy the code

2.2 Using WorkManager (Java)

2.2.1 Defining tasks

The definition task must inherit from the Worker class and call its constructor (unique). Override the doWork() method.

Note: The doWork() method runs asynchronously on a background thread provided by the WorkManager, so you can safely perform time-consuming operations here.

/** * Create time: 2022/1/27 * Function: Define a task */
public class JavaTestWork extends Worker {
    public JavaTestWork(@NonNull @NotNull Context context, @NonNull @NotNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @NotNull
    @Override
    public Result doWork(a) {
        // Perform time-consuming operations
        doSmothing();
        return Result.success();
    }
    private void doSmothing(a){
        // This is just a simple print
        Log.e("JavaTestWork"."JavaTestWork performing doWork"); }}Copy the code

The Result returned from doWork() informs the WorkManager service whether the work was successful and whether the work should be retried in case of failure:

  • Result.success() : success.
  • Result.failure() : failure.
  • Result.retry() : fails. The task can be re-executed using the setBackoffCriteria() method of workRequest.Builder.

2.2.2 Build task request

WorkRequest.Worker defines the unit of work, and WorkRequest (and its subclasses) defines how and when it should run. In the simplest case, you can use OneTimeWorkRequest.

        // Build the task request
        // Method 1: simple Work without additional configuration
        WorkRequest fromWorkRequest = OneTimeWorkRequest.from(JavaTestWork.class);
        // Method 2: complex Work, which can be configured using the builder
        WorkRequest workRequest =
                new OneTimeWorkRequest.Builder(JavaTestWork.class).build();
Copy the code

2.2.3 Submitting WorkRequest to the System

Finally, the WorkRequest built tasks are submitted to the WorkManager using the enqueue() method. The system will run at the right time.

        binding.btnDowork.setOnClickListener(view -> {
            Snackbar.make(view, "Click doWork", Snackbar.LENGTH_LONG)
                    .setAction("Action".null).show();
            // Submit WorkRequest to the system
            WorkManager.getInstance(JavaWorkActivity.this).enqueue(workRequest);
        });
Copy the code

Advanced usage of WorkManager

While this is just a simple use of WorkMannager, let’s take a look at how you can use WorkManager to handle complex tasks.

3.1 Scheduled Task

We’ve used this a lot in projects, so let’s take a look. This can be done by using the setInitialDelay() method while building the task.

        // Build complex tasks
        WorkRequest timedWorkRequest =
                new OneTimeWorkRequest.Builder(JavaTestWork.class)
                        // Execute after 1 minute, of course you can specify the unit with this method (ms/s/min/hour/day)
                        .setInitialDelay(1, TimeUnit.MINUTES)
                        .build();
        binding.btnTimedwrok.setOnClickListener(v -> {
            Log.e("btnTimedwrok"."start");
            WorkManager.getInstance(JavaWorkActivity.this).enqueue(timedWorkRequest);
            Log.e("btnTimedwrok"."end");
        });
Copy the code

It turned out to be a minute later.

3.2 Canceling or Stopping a Task

There are circumstances that require us to cancel our submitted tasks. It can be cancelled by its name, ID, or tag.

3.2.1 Canceling a Task by Id

        binding.btnCancelWork.setOnClickListener(v -> {
            Log.e("btnCancelWork"."cancel");
            // Cancel the task by id
            workManager.cancelWorkById(timedWorkRequest.getId());
        });
Copy the code

3.2.2 Canceling a Task Using a Tag

        WorkRequest timedWorkRequest =
                new OneTimeWorkRequest.Builder(JavaTestWork.class)
                        // Execute after 1 minute, of course you can specify the unit with this method (ms/s/min/hour/day)
                        .setInitialDelay(1, TimeUnit.MINUTES)
                        // Add a tag that can be used to cancel a task
                        .addTag("timedWrok")
                        .build();
        binding.btnCancelWork.setOnClickListener(v -> {
            Log.e("btnCancelWork"."cancel");
            // Cancel the task with a tag
            workManager.cancelAllWorkByTag("timedWrok");
        });
Copy the code

3.2.3 Canceling All Tasks

            // Cancel all tasks
            workManager.cancelAllWork();
Copy the code

3.3 Observation Tasks

It is also easy to observe task information. You can use getWorkInfoBy… () or getWorkInfoBy… LiveData() method and gets a reference to WorkInfo.

Let’s take a look at the progress and return status of the task.

        // Observing
        WorkRequest observeWork = new OneTimeWorkRequest.Builder(JavaTestWorkInfo.class)
                .build();
        binding.btnObserveWorkinfo.setOnClickListener(v -> {
            Log.e("btnObserveWorkinfo"."start");
            workManager.getWorkInfoByIdLiveData(observeWork.getId())
                    .observe(this, workInfo -> {
                        if(workInfo ! =null) {
                            Data progress = workInfo.getProgress();
                            int value = progress.getInt("PROGRESS".0);
                            if (value>0) {
                                Log.e("MainObserve"."Current Progress:" + value);
                            }
                            if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
                                Log.e("MainState"."Success");
                            } else if (workInfo.getState() == WorkInfo.State.FAILED) {
                                Log.e("MainState"."Failure");
                            } else if (workInfo.getState() == WorkInfo.State.CANCELLED) {
                                Log.e("MainState"."Cancel"); }}else {
                            Log.e("MainObserve"."workInfo == null"); }});// Don't forget to add to the WorkManager queue
            workManager.enqueue(observeWork);
        });
    }
Copy the code

Let’s look at the Worker using the setProgressAsync() API to update progress.

public class JavaTestWorkInfo extends Worker {
    private static final String PROGRESS = "PROGRESS";
    private static final long DELAY = 5000L;
    public JavaTestWorkInfo(@NonNull @NotNull Context context, @NonNull @NotNull WorkerParameters workerParams) {
        super(context, workerParams);
        Log.e("JavaTestWorkInfo"."Construction method");
        setProgressAsync(new Data.Builder().putInt(PROGRESS,10).build());
    }

    @NonNull
    @NotNull
    @Override
    public Result doWork(a) {
        // This is just a simple print
        Log.e("JavaTestWorkInfo"."Performing doWork");
        // Perform time-consuming operations
        doSmothing();
        try {
            Log.e("JavaTestWorkInfo"."Result.success");
            setProgressAsync(new Data.Builder().putInt(PROGRESS,100).build());
            Thread.sleep(DELAY);
        } catch (InterruptedException exception) {
            // ... handle exception
        }
        return Result.success();
    }
    private void doSmothing(a){
        try {
            setProgressAsync(new Data.Builder().putInt(PROGRESS,40).build());
            Thread.sleep(DELAY);
        } catch (InterruptedException exception) {
            // ... handle exception}}}Copy the code

Does this progress help you a lot when you upload and download resources?

3.4 Performing the Failed Task again

When the background task doWork() returns result.retry () fails, the task can be re-executed using the setBackoffCriteria() method of workRequest.Builder.

        WorkRequest retryWork = new OneTimeWorkRequest.Builder(JavaTestWorkRetry.class)
                .setBackoffCriteria(BackoffPolicy.LINEAR,10,TimeUnit.SECONDS)
                .build();
        binding.btnReturnRetry.setOnClickListener(v -> {
            Log.e("btnReturnRetry"."start");
            workManager.getWorkInfoByIdLiveData(retryWork.getId())
                    .observe(this, workInfo -> {
                        if(workInfo ! =null) {
                            / /...
                        } else {
                            Log.e("btnReturnRetry"."workInfo == null"); }});// Don't forget to add to the WorkManager queue
            workManager.enqueue(retryWork);
        });
Copy the code
  • The first argument: BackoffPolicy has two values:

    • Backoffpolicy. LINEAR(The time of each retry increases linearly, such as 10 seconds for the first retry and 20 seconds for the second retry)

    • BackoffPolicy. An EXPONENTIAL increase (each retry time index).

  • The second and third parameters: specify how long to wait for the task to resume. The minimum time must be 10 seconds.

3.5 the value

  • Pass the Activity’s data to the task.
  • Pass the data from the task to the Activity.
        // Pass in the Worker value
        Data data = new Data.Builder()
                .putInt("age".20)
                .putString("name"."Scc").build();
        WorkRequest inputWork = new OneTimeWorkRequest.Builder(JavaTestWorkInput.class)
                .setInputData(data)
                .build();
        binding.btnInputData.setOnClickListener(v -> {
            Log.e("btnInputData"."start");
            workManager.getWorkInfoByIdLiveData(inputWork.getId())
                    .observe(this, workInfo -> {
                        if(workInfo ! =null) {
                            / /...
                            Data outputData = workInfo.getOutputData();
                            String str = outputData.getString("work") +":"+outputData.getInt("price", -50);
                            if (outputData.getString("work")! =null) {
                                Log.e("btnInputData", str); }}else {
                            Log.e("btnInputData"."workInfo == null"); }});// Don't forget to add to the WorkManager queue
            workManager.enqueue(inputWork);
        });
        
public class JavaTestWorkInput extends Worker {...public Result doWork(a) {
        // This is just a simple print
        Log.e("JavaTestWorkInput"."Performing doWork");
        // Accept the value passed from the Activity
        Data data = getInputData();
        / / print
        String msg = data.getString("name") +"This year"+data.getInt("age".5) +" 岁了";
        Log.e("JavaTestWorkInput",msg);
        Data outputData  = new Data.Builder()
                .putString("work"."I return the favor.")
                .putInt("price".500)
                .build();
        returnResult.success(outputData); }}Copy the code

3.6 Constraints

        Constraints constraints = new Constraints.Builder()
                .setRequiresDeviceIdle(true)// Whether the device is idle when triggered
                .setRequiresCharging(true)// Whether the device is charged when triggered
                .setRequiredNetworkType(NetworkType.UNMETERED)// Network status when triggered
                .setRequiresBatteryNotLow(true)// Specify whether the device battery should not fall below the critical threshold
                .setRequiresStorageNotLow(true)// Specifies whether the available storage of the device should not fall below the critical threshold
AddContentUriTrigger (myUri,false)// Whether {@link WorkRequest} updates should be run when specifying content {@link android.net.Uri}
                .build();
        WorkRequest inputWork = new OneTimeWorkRequest.Builder(JavaTestWorkInput.class)
                .setInputData(data)
                .setConstraints(constraints)
                .build();
Copy the code

3.7 Chain tasks

Suppose we have three separate background tasks: get an image, compress an image, and upload an image. So if we want to do this, we can do it using chained tasks.

        // chain call
        workManager
                // Run parallel
                .beginWith(Arrays.asList(work1, work2, work3))
                // Continue to invoke the compression task after performing the signature operation
                .then(compression)
                // The upload task is invoked after the compression task is executed
                .then(upload)
                // Add to the WorkManager queue
                .enqueue();
Copy the code
  • BeginWith: beginWith() means to begin a chained task.
  • Then: Use the then() method to link any subsequent tasks.