The use of the WorkManager

Import libraries

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


dependencies {
    def work_version = "2.5.0" // The latest stable version
    // (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

Define work

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

To create some work for WorkManager to run, extend the Worker class and replace 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
     XXXXXXXXXX

     // Indicate whether the work finished successfully with the Result
     returnResult.success(); }}Copy the code

The Result returned from doWork() informs the WorkManager service whether the work was successful and whether the work should be retried if the work failed.

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

Define WorkRequest

Once the work is defined, it must be scheduled using the WorkManager service before it can run. WorkManager provides a great deal of flexibility in how work is scheduled. You can schedule it to run regularly over a certain period of time, or you can schedule it to run only once.

Regardless of how you choose to schedule work, always use WorkRequest. The Worker defines the unit of work, and the WorkRequest (and its subclasses) defines how and when the work is run.

WorkRequest itself is an abstract base class. There are two derived implementations of this class that can be used to create OneTimeWorkRequest and PeriodicWorkRequest requests. As the name suggests, OneTimeWorkRequest is good for scheduling non-repetitive work, while PeriodicWorkRequest is better for scheduling work that is repeated at regular intervals.

Scheduling one-off work

For simple work that requires no additional configuration, use the static method from:

WorkRequest uploadWorkRequest = OneTimeWorkRequest.from(UploadWorker.class);
Copy the code

For more complex work, you can use a builder.

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

Schedule regular work

Your application may sometimes need to run some work on a regular basis. For example, you might have to periodically back up data, periodically download fresh content from your application, or periodically upload logs to the server.

Use PeriodicWorkRequest to create a WorkRequest object that is periodically executed as follows:

PeriodicWorkRequest saveRequest =
       new PeriodicWorkRequest.Builder(SaveImageToFileWorker.class, 1, TimeUnit.HOURS) // Work runs at intervals of one hour
           // Constraints
           .build();
Copy the code

※ Note: The minimum repetition interval that can be defined is 15 minutes.

If the nature of your work makes it runtime sensitive, you can configure PeriodicWorkRequest to run in flexible periods at each interval, as shown in the figure below.

To define regular work with flexible hours, click CreatePeriodicWorkRequestWhen passingflexIntervalAs well asrepeatInterval. Flexible time rangerepeatInterval - flexIntervalStart, and continue until the interval is over.

Here is an example of a regular job that can run in the last 15 minutes of each hour.

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

※ Note: The minimum repetition interval that can be defined is 15 minutes. The flexible period must be greater than or equal to 5 minutes.

The WorkRequest is submitted to the system

Finally, you need to submit the WorkRequest to the WorkManager using the enqueue() method.

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

Let’s add a Log input statement to the doWork() method of work.

public class UploadWorker extends Worker {
    public Result doWork(a) {
        Log.d("test"."i is " + i);
        Log.d("test"."j is " + j);
        i++;
        j++;
        return Result.success();
    }
}

WorkRequest uploadWorkRequest = OneTimeWorkRequest.from(UploadWorker.class);
WorkManager.getInstance(getBaseContext())
                .enqueue(uploadWorkRequest);
Copy the code

When using OneTimeWorkRequest, the Log does only print once, no matter how long you wait.

When using PeriodicWorkRequest, the repeat execution time is set to 15 minutes.

public class UploadWorker extends Worker {
    public Result doWork(a) {
        Log.d("test"."i is " + i);
        Log.d("test"."j is " + j);
        i++;
        j++;
        return Result.success();
    }
}

WorkRequest uploadWorkRequest2 = new PeriodicWorkRequest.Builder(UploadWorker.class,
                15, TimeUnit.MINUTES)
                .build();
WorkManager.getInstance(getBaseContext())
                .enqueue(uploadWorkRequest2);
Copy the code

Run the Log command as follows:

The Log output time shows that UploadWorker is executed again after 15 minutes after the first execution.

And from the above Log, we also found that UploadWorker rebuilt a UploadWorker object every time it was executed. So PeriodicWorkRequest repeats the task once, rather than following up on the previous one.

We also tested that after killing the program from the background, the Log can output again after an interval of 15 minutes.

Working status of the WorkRequest

After the WorkRequest is submitted to the system, what is the working state of the WorkRequest in the system?

Working at one time

For a one-time work request, the initial state of the work is ENQUEUED.

In ENQUEUED state, your work is executed as soon as work constraints and work delays are met. The work then changes to the RUNNING state, and then to SUCCEEDED or FAILED based on the results of the work. Or, if the result is retry, it may return to ENQUEUED. You may cancel at any time during the process and will be CANCELLED.

Both SUCCEEDED and FAILED and CANCELLED say the work State of termination, and WorkInfo. State. IsFinished () returns true.

The state of working regularly

For PeriodicWorkRequest, there was only one CANCELLED state. SUCCEEDED and FAILED apply only to the one-time work. That’s because regular work never ends, except for cancellations. After each run, regardless of the outcome, the system rescheduled it.

WorkManager inputs and outputs

The WorkManager described above is a simple example that outputs a Log to the Worker without any interaction with the Activity. In practice, however, we need to interact with the Activity to inform the user that there is a time-consuming task running, and so on.

In input and output, input values are stored in Data objects as key-value pairs and can be set in work requests. The WorkManager passes the input Data to the work as it executes. The Worker class accesses input parameters by calling worker.getinputData ().

        String[] strings = {"AAA","BBB","CCC","DDD"};
        Data data = new Data.Builder().putStringArray("STRING_ARRAY_KEY", strings).build();
        WorkRequest uploadWorkRequest = new OneTimeWorkRequest.Builder(UploadWorker.class)
                .setInputData(data)
                .build();
​
        WorkManager.getInstance(getBaseContext()).enqueue(uploadWorkRequest);
​
        WorkManager.getInstance(getBaseContext()).getWorkInfoByIdLiveData(uploadWorkRequest.getId())
                .observe(this, info -> {
                    if (info != null && info.getState() == WorkInfo.State.SUCCEEDED) {
                        String[] myResult = info.getOutputData().getStringArray("STRING_ARRAY_KEY_OUT");
                        // ... do something with the result ...
                        String string = "";
                        Log.d("test","myResult is " + myResult.toString());
                        for (int i = 0; i < myResult.length; i++) {
                            string = string + myResult[i] + "\n";
                        }
                        textView.setText(string);
                    }
                });
Copy the code

The above code is the code of Worker’s requestor, in which we create a Data in the form of a character array, define a OneTimeWorkRequest, and pass the Data to the work through the setInputData method. It also defines a return listener (getWorkInfoByIdLiveData). In the listener, To determine whether the State of WorkInfo(Info) is Successded, return the value (getOutputData()) from WorkInfo again.

public class UploadWorker extends Worker { public UploadWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); Log.d("test", "new UploadWorker"); } @NonNull @Override public Result doWork() { String[] strings = getInputData().getStringArray("STRING_ARRAY_KEY"); Log.d("test","strings is " + strings.toString()); String[] strings1 = new String[strings.length]; for(int i = 0; i < strings.length; i++) { strings1[i] = strings[i] + i; } Log.d("test","strings1 is " + strings1.toString()); Data outData = new Data.Builder().putStringArray("STRING_ARRAY_KEY_OUT", strings1).build(); return Result.success(outData); }}Copy the code

In the UploadWorker, the getInputData method is used to get the Data passed from the requester. At the same time, a new Data is created to pass the Data to the requester. The requester gets the returned data through the return listener and the getOutputData method.

The above example code executes as follows:

The example code above uses OneTimeWorkRequest, so could we use PeriodicWorkRequest in the same way?

The code for UploadWorker remains unchanged, and the requester changes the code as follows:

String[] strings = {"AAA","BBB","CCC","DDD"}; Data data = new Data.Builder().putStringArray("STRING_ARRAY_KEY", strings).build(); WorkRequest uploadWorkRequest2 = new PeriodicWorkRequest.Builder(UploadWorker.class, 15, TimeUnit.MINUTES) .setInputData(data) .build(); WorkManager.getInstance(getBaseContext()).enqueue(uploadWorkRequest2); WorkManager.getInstance(getBaseContext()).getWorkInfoByIdLiveData(uploadWorkRequest2.getId()) .observe(this, info -> { if (info ! = null && info.getState() == WorkInfo.State.SUCCEEDED) { ...... Log.d("test","myResult is " + myResult.toString());Copy the code

The execution results show no changes to the interface and no output of the Log in the above code. This indicates that the condition for judging whether “state” is SUCCEEDED is not met. Check the description of workinfo.state.SUCCEEDED, and you can find that PeriodicWorkRequest never returns to the SUCCEEDED state.

        /**
         * Used to indicate that the {@link WorkRequest} has completed in a successful state.  Note
         * that {@link PeriodicWorkRequest}s will never enter this state (they will simply go back
         * to {@link #ENQUEUED} and be eligible to run again).
         */
        SUCCEEDED,
Copy the code

Based on the comments above, let’s change the criteria to if (info! = null && info.getState() == workinfo.state.enqueued);

Shutting down VM 06-11 19:43:03.002 30439 30439 D AndroidRuntime: Shutting down VM 06-11 19:43:03.005 30439 30439 E AndroidRuntime: Shutting down VM 06-11 19:43:03.005 30439 30439 E AndroidRuntime: FATAL EXCEPTION: the main 06-11 19:43:03. 005 30439 30439 E AndroidRuntime: Process: com. Example. Workmanagersimple, PID: 30439-06-11 19:43:03. 005 30439 30439 E AndroidRuntime: Java. Lang. NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.Object.toString()' on a null object reference 06-11 19:43:03.005 30439 30439 E AndroidRuntime: At com. Example. Workmanagersimple. MainActivity. Lambda $onCreate $0 (44) MainActivity. Java: 6-11 19:43:03. 005, 30439, 30439 E AndroidRuntime: at com.example.workmanagersimple.-$$Lambda$MainActivity$w1mLUdtnmzEu1slhnhK3ua3n61o.onChanged(Unknown Source:4)Copy the code

From the above Error message, we can know two things:

1. The current state of the program is ENQUEUED.

MyResult = null; myResult = null;

Why is myResult null? The return value is already passed through result.success (outData). View the description of WorkInfo.

Information about a particular WorkRequest containing the id of the WorkRequest, its current WorkInfo.State, output, tags, and run attempt count. Note that output is only available for the terminal states (WorkInfo.State.SUCCEEDED and WorkInfo.State.FAILED).
Copy the code

WorkInfo specifies that output can only be passed in the workinfo.state. SUCCEEDED and workinfo.state. FAILED states. The current State of PeriodicWorkRequest is workinfo.state. ENQUEUED, so the output of PeriodicWorkRequest cannot be obtained through WorkInfo.

So can PeriodicWorkRequest get the return value? I’ll check the online instructions. Some articles say to use the following method.

        WorkManager.getInstance().getStatusById(request.getId()).observe(this, new Observer<WorkStatus>() {
            @Override
            public void onChanged(@Nullable WorkStatus workStatus) {
Copy the code

However, the getStatusById method is not found in the WorkManager function, and there is no WorkStatus class. Also set the work-time version in AndroidStudio to the lowest possible 2.0.0. There is no WorkStatus class or getStatusById method.

On the other hand, although PeriodicWorkRequest can’t get the return value directly, can we use an indirect method? For example, in the Worker’s doWork method, write the execution result into a file or database, and then judge the state at the requester; When the state meets the condition, the data is read from the file or database. This is also a way to get data.

The cancellation of the WorkManager

In the above work status, we know that there is a CANCELLED state whether it is a one-time job or a regular job. So CANCELLED is very important in the WorkManager.

There are four cancel methods in the WorkManager: cancelAllWork, cancelAllWorkByTag, cancelWorkById and cancelUniqueWork.

cancelAllWork()

This function cancels all work that does not have a finish

Data data1 = new Data.Builder().putString("key", "1").build(); WorkRequest uploadWorkRequest1 = new OneTimeWorkRequest.Builder(UploadWorker.class) .setInitialDelay(10, TimeUnit.MINUTES) .setInputData(data1) // Additional configuration .build(); Data data2 = new Data.Builder().putString("key", "2").build(); WorkRequest uploadWorkRequest2 = new OneTimeWorkRequest.Builder(UploadWorker.class) .setInitialDelay(10, TimeUnit.MINUTES) .setInputData(data2) // Additional configuration .build(); WorkManager workManager = WorkManager.getInstance(MainActivity.this); workManager.enqueue(uploadWorkRequest1); workManager.getWorkInfoByIdLiveData(uploadWorkRequest1.getId()).observe(this, info -> { if (info ! = null && info.getState() == WorkInfo.State.CANCELLED) { Log.d(TAG, "WorkRequest 1 is cancelled"); }}); workManager.enqueue(uploadWorkRequest2); workManager.getWorkInfoByIdLiveData(uploadWorkRequest2.getId()).observe(this, info -> { if (info ! = null && info.getState() == WorkInfo.State.CANCELLED) { Log.d(TAG, "WorkRequest 2 is cancelled"); }}); Log.d(TAG, "WorkRequest 1 & 2 have submit to System"); Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { workManager.cancelAllWork(); }}, 5000);Copy the code

In the above code, we defined two workrequests and set a delay to facilitate us to cancel these two works. After all, we can only cancel the unfinished work. Then we define a Handler that delays the cancel operation for 5 seconds. The printed log is as follows:

The 2021-01-06 20:41:01. 553, 15367-15367 / com. Example. Myapplication D/MainActivity: WorkRequest 1 & 2 have submit to System 20:41:06. 2021-01-06, 665, 15367-15367 / com. Example. Myapplication D/MainActivity: WorkRequest 1 is cancelled the 2021-01-06 20:41:06. 667, 15367-15367 / com. Example. Myapplication D/MainActivity: WorkRequest 2 is cancelledCopy the code

Both works are indeed cancelled.

cancelAllWorkByTag(@NonNull String tag)

This function cancels all work that does not have a finish and that matches the given work tag

WorkRequest uploadWorkRequest1 = ...... .addTag("work") WorkRequest uploadWorkRequest2 = ...... .addTag("work") WorkRequest uploadWorkRequest3 = ...... .addTag("work_work") handler.postDelayed(new Runnable() { @Override public void run() { workManager.cancelAllWorkByTag("work"); }}, 5000);Copy the code

Add a new WorkRequest3, add another Tag(work_work), and then call cancelAllWorkByTag (work_work) in handler. Cancel the work that is tagged as work. The printed Log is as follows:

The 2021-01-06 21:02:42. 256, 15888-15888 / com. Example. Myapplication D/MainActivity: WorkRequest 1 & 2 & 3 have submit to System 21:02:47. 2021-01-06, 317, 15888-15888 / com. Example. Myapplication D/MainActivity: WorkRequest 1 is cancelled the 2021-01-06 21:02:47. 319, 15888-15888 / com. Example. Myapplication D/MainActivity: WorkRequest 2 is cancelled the 2021-01-06 21:02:52. 372, 15888-15969 / com. Example. Myapplication D/MainActivity: UploadWorker doWork and key is 3Copy the code

WorkRequest1 and 2 with the work Tag are cancelled, but WorkRequest3 with the work_work Tag is completed.

cancelWorkById(@NonNull UUID id)

This function cancels any work that does not have a finish and whose ID matches the given work

handler.postDelayed(new Runnable() { @Override public void run() { workManager.cancelWorkById(uploadWorkRequest3.getId()); }}, 5000);Copy the code

This time we call the cancelWorkById function in Handler, given the ID of WorkRequest3. Print the Log as follows:

The 2021-01-06 21:18:04. 358, 16367-16367 / com. Example. Myapplication D/MainActivity: WorkRequest 1 & 2 & 3 have submit to System 21:18:09. 2021-01-06, 451, 16367-16367 / com. Example. Myapplication D/MainActivity: WorkRequest 3 is cancelled the 2021-01-06 21:18:14. 415, 16367-16421 / com. Example. Myapplication D/MainActivity: UploadWorker doWork and key is 1 2021-01-06 21:18:14. 426, 16367-16422 / com. Example. Myapplication D/MainActivity: UploadWorker doWork and key is 2Copy the code

Only WorkRequest3 is cancelled. Workrequest1&2 completes normally.

※ Note: The cancelWorkById function does not cancel multiple works because each WorkRequest is defined with a given ID.

cancelUniqueWork(@NonNull String uniqueWorkName)

This function cancels if there is no finish and the name matches the given work

workManager.enqueueUniqueWork("work&work", ExistingWorkPolicy.KEEP, (OneTimeWorkRequest) uploadWorkRequest3); handler.postDelayed(new Runnable() { @Override public void run() { workManager.cancelUniqueWork("work&work"); }}, 5000);Copy the code

To use this method, we need to change where the work is submitted. In the previous methods, we used the enquen() method to submit the work, but in this case we need to use the enqueueUniqueWork() method, The first argument to this method is the name we need for the cancelUniqueWork method.

With reference to the literature: developer. The android. Google. Cn/jetpack/and… Developer. The android. Google. Cn/topic/libra…