Key features of WorkManager

  • Backward compatibility to API14
    • JobScheduler is used above API 23
    • Use a combination of BroadcastReceiver and AlarmManager between apis 14 and 22
  • You can add task constraints, such as network or charging status
  • You can schedule one-off or periodic asynchronous tasks
  • Can monitor and manage tasks that need to be scheduled
  • You can link tasks together
  • Ensure that the task is executed even if the app or device is restarted
  • Abide by power-saving features such as Doze mode

WorkManager is designed for deferred tasks that do not need to be executed immediately, but need to be guaranteed to be executed even if the application exits or the device restarts. Such as:

  • Send logs or analysis to the background service
  • Periodically synchronize data with the server

WorkManager is not designed for in-process background tasks that are stopped when the app process exits, nor for tasks that need to be executed immediately.

Use the WorkManager

Statement depends on

dependencies {
  def work_version = "2.2.0"

    // (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"
  }
Copy the code

Creating a Background Task

Inherit Worker and rewrite doWork()

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

    @NonNull
    @Override
    public Result doWork(a) {
        //business logic
        returnResult.success(); }}Copy the code

Result Returns three types of results:

  • Execute successfully, result.success () or result.success (data)
  • Failure to execute, result.failure () or result.failure (data)
  • Need to retry, result.retry ()

Configuring Tasks

Worker defines specific tasks, and WorkRequest defines how and when to perform tasks. TimeWorkRequest is used for one-off tasks, and PeriodicWorkRequest is used for periodic tasks.

OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class).build();
Copy the code
PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(UploadWorker.class, 10, TimeUnit.SECONDS).build();
Copy the code

Scheduling WorkRequest

Call the enqueue method of the WorkManager

WorkManager.getInstance(ctx).enqueue(uploadReq);
Copy the code

The specific execution timing of the task depends on the constraints set by WorkRequest and system optimization.

Define WorkRequest

You can customize WorkRequest to solve the following scenarios:

  • Add constraints to the task, such as network state
  • Ensure the minimum delay of task execution
  • Handles retry and compensation of tasks
  • Handles the input and output of tasks
  • Label a group of tasks

Task constraints

Add constraints to a task to indicate when the task can be executed.

For example, you can specify that tasks can only be performed when the device is idle or connected to power.

Constraints constraints = new Constraints.Builder()
				// When the local contentURI is updated, the task is triggered (API must be greater than or equal to 24, in accordance with JobSchedule).
                .addContentUriTrigger(Uri.EMPTY, true)
    			// Maximum delay for executing tasks when content URI changes, in conjunction with JobSchedule
                .setTriggerContentMaxDelay(10, TimeUnit.SECONDS)
    			// Delay in executing tasks when content URI updates (API >=26)
                .setTriggerContentUpdateDelay(100, TimeUnit.SECONDS)
    			// Network status of the task: no network requirement, network connection, unlimited network, non-mobile network, pay-per-traffic network
                .setRequiredNetworkType(NetworkType.NOT_ROAMING)
    			// The power is sufficient to execute
                .setRequiresBatteryNotLow(true)
    			// Can only be executed while charging
                .setRequiresCharging(false)
    			// Only when the storage space is sufficient
    			.setRequiresStorageNotLow(false)
    			// Execute only when the device is idle
                .setRequiresDeviceIdle(true)
                .build();
Copy the code

When multiple constraints are set, the task is executed only when these conditions are met.

When a task is running, WorkManager terminates the task if the constraints are not met. These tasks are retried the next time the constraint is satisfied.

Lazy initialization

Tasks may be executed immediately if they have no constraints or if the constraints are met. If you do not want the tasks to be executed immediately, you can delay the execution of these tasks for a certain amount of time.

OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class).setInitialDelay(10, TimeUnit.SECONDS).build();
Copy the code

Retry and compensation policies

If you need WorkManager to retry a task, you can have the task return result.retry ().

Tasks are rescheduled and there is a default compensation delay and policy. Compensation delay specifies a minimum wait time for a task to be retried. The supplement policy defines how the compensation delay will increase over the next few retries. The default is exponential.

OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class).setInitialDelay(10, TimeUnit.SECONDS)
                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10 ,TimeUnit.SECONDS)
                .build();
Copy the code

Define inputs and outputs

Tasks may need to pass in data as input parameters or return result data. For example, a task that uploads an image requires the URI of the image, and possibly the address where the image was uploaded.

Input and output values are stored as key-value pairs in Data objects.

Data data = new Data.Builder().putString("key1"."a").build();
OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class)
                .setInputData(data)
                .build();
Copy the code

The Wroker class calls worker.getinputData () to get input parameters.

The Data class can also be used as output. Return a Data object in the Worker by calling result.success (Data) or result.failure (Data).

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

    @NonNull
    @Override
    public Result doWork(a) {
        //business logic
        Data data = new Data.Builder().putString("image-url"."http://xxxx.png").build();
        returnResult.success(data); }}Copy the code

Mark task

For any WorkRequest object, you can logically turn a set of tasks into a group by assigning a label to them. This allows you to manipulate all the tasks for a particular tag.

For example, the WorkManager. CancelAllWorkByTag (String) cancelled all the label tasks; The WorkManager. GetWorkInfosByTagLiveData (String) returns a LiveData contains all tasks under the label status list

OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class)
                .addTag("upload")
                .build();
Copy the code

Status of tasks and observation tasks

Task status

During the life cycle of a task, it passes through various states:

  • BLOCKED, when a task’s prerequisites have not been met
  • ENQUEUED: indicates that a task is in the queue state when the constraints and time of the task are enough to execute
  • RUNNING: When a task is being executed
  • SUCCEEDED, a task returns result.success () and is in a successful state. This is the end state; Only one-time tasks (OneTimeWorkRequest) can reach this state
  • FAILED, a task that returns result.failure () is in a FAILED state. This is also an end state; Only one-time tasks (OneTimeWorkRequest) can reach this state. All tasks that depend on it are marked FAILED and will not be executed
  • CANCELLED, an unterminated WorkRequest is in the CANCELLED state when explicitly CANCELLED. All tasks that rely on it will also be marked CANCELLED and will not be executed

Observe the task

WorkManager allows you to check the status of tasks when they are queued. This information can be obtained through the WorkInfo object and contains the task ID, tag, current State, and output data.

There are several ways to obtain WorkInfo:

  • For specific WorkRequest, can get it through id WorkInfo, call the WorkManager. GetWorkInfoById (id) or WorkManager. GetWorkInfoByIdLiveData (id)
  • For a given tag, you can get all the matches the tag task WorkInfo object, call the WorkManager. GetWorkInfosByTag (tag) or WorkManager. GetWorkInfosByTagLiveData (tag)
  • For a unique task name, you can get the WorkInfo objects for all matching tasks, Call the WorkManager. GetWorkInfosForUniqueWork (name) or WorkManager. GetWorkInfosForUniqueWorkLiveData (name)

The LiveData returned by the above method can be registered with a listener to observe changes to WorkInfo.

 WorkInfo workInfo = WorkManager.getInstance(this).getWorkInfoById(UUID.fromString("uuid")).get();
        WorkManager.getInstance(this).getWorkInfoByIdLiveData(UUID.fromString("uuid")).observe(this.new Observer<WorkInfo>() {
            @Override
            public void onChanged(WorkInfo workInfo) {}});Copy the code

Observe the progress of the task

WorkManager 2.3.0-Alpha01 adds support for setting and observing the progress of tasks. If the application is in the foreground while the task is running, progress information can be displayed to the user via LiveData of WorkInfo returned by the API.

ListenableWorker now supports setProgressAsync(), which saves intermediate progress. These apis enable developers to set progress so that it can be displayed on the UI. Progress is represented by a Data type, which is a serializable property container (like inputs and outputs, subject to the same constraints).

Progress information can only be observed and updated while ListenableWorker is running. The setting progress is ignored when ListenableWorker ends. By calling getWorkInfoBy.. () or getWorkInfoBy… LiveData() interface to view progress information. These methods return an object instance of WorkInfo, and they have a new getProgress() method that returns a Data object.

Update progress

A developer using ListenableWorker or the Worker, setProgressAsync() interface returns a ListenableFuture; Update progress is asynchronous and involves storing progress information to the database. In Kotlin, progress information can be updated using the setProgress() extension method of the CoroutineWorker object.

public class ProgressWorker extends Worker {
    public ProgressWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
        setProgressAsync(new Data.Builder().putInt("progress".0).build());
    }

    @NonNull
    @Override
    public Result doWork(a) {
        setProgressAsync(new Data.Builder().putInt("progress".100).build());
        returnResult.success(); }}Copy the code

To observe the progress

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

WorkRequest progress = new OneTimeWorkRequest.Builder(ProgressWorker.class).addTag("progress").build();
        WorkManager.getInstance(this).getWorkInfoByIdLiveData(progress.getId()).observe(this.new Observer<WorkInfo>() {
            @Override
            public void onChanged(WorkInfo workInfo) {
                int progress = workInfo.getProgress().getInt("progress".0); }});Copy the code

Link to work

Introduction to the

WorkManager allows you to create and enqueue a series of tasks, specify multiple dependent tasks, and the order in which they are executed. This is useful if you want to perform multiple tasks in a particular order.

To create a series of tasks, you can use workManager.BeginWith (OneTimeWorkRequest) or workManager.BeginWith (List), which return a WorkContinuation instance.

A WorkContinuation instance can then be used to add a dependent OneTimeWorkRequest, By calling workContainment.then (OneTimeWorkRequest) or workContinuation. then(List).

Each WorkContinuation. Then (…). Returns a new WorkContinuation instance. If the list of OneTimeRequest is added, these requests might run serially.

Finally, you can queue the WorkContinuation chain with the workContinuation.enqueue () method.

WorkManager.getInstance(myContext)
    // Candidates to run in parallel
    .beginWith(Arrays.asList(filter1, filter2, filter3))
    // Dependent work (only runs after all previous work in chain)
    .then(compress)
    .then(upload)
    // Don't forget to enqueue()
    .enqueue();
Copy the code

Input to merge

When a chained OneTimeWorkRequest is used, the output from the parent OneTimeWorkRequest is used as input to the child task. So the output of filter1, filter2, and filter3 in the preceding example will be used as input to the COMPRESS task.

To manage input from multiple parent tasks, The WorkManager uses InputMerger for input merging.

WorkManager offers two different types of InputMerger:

  • The OverwritingInputMerger attempts to add keys from all inputs to the output. When a key conflict occurs, the previous key is overwritten.
  • ArrayCreatingInputMerger attempts to merge all inputs into an array if necessary.
OneTimeWorkRequest compress =
    new OneTimeWorkRequest.Builder(CompressWorker.class)
        .setInputMerger(ArrayCreatingInputMerger.class)
        .setConstraints(constraints)
        .build();
Copy the code

Status of links and tasks

There are a few things to keep in mind when creating a OneTimeWorkRequest task chain:

  • The child OneTimeWorkRequest is non-blocking (transitioning to ENQUEUED state) when all parent OneTimeWorkRequests are successfully executed.
  • When any parent OneTimeWorkRequest fails to execute, all dependent OneTimeWorkRequests are marked FAILED.
  • When any parent OneTimeWorkRequest is CANCELED, all OneTimeWorkRequests that depend on it are marked as CANCELED.

Cancels and terminates tasks

If the enqueue task is no longer needed, it can be cancelled. To cancel a single WorkRequest the simplest method is to use the id and invoke the WorkManager. CancenWorkById (UUID).

WorkManager.cancelWorkById(workRequest.getId());
Copy the code

At the bottom, the WorkManager checks the status of tasks. If the task has been completed, nothing has happened. Otherwise, the status of the task moves to CANCELED and the task does not run again. Any other WorkRequest that relies on this task is marked as CANCELED.

In addition, if the current task is running, the task will trigger ListenableWorker. OnStopped () callback. Override this method to handle any possible cleanup.

Tags can also be used to cancel the task, by calling the WorkManager. CancelAllWorkByTag (String). Note that this method cancels all tasks with this label. In addition, also can call the WorkManager. CancelUniqueWork (String) to cancel all tasks with the unique name.

Terminates a running task

There are several cases where running tasks are terminated by WorkManager:

  • The cancel task method is explicitly called
  • Task constraints are never satisfied again
  • The system terminated the application for some reason. This can happen if the last execution time is more than 10 minutes. The task will then be scheduled for retry.

In these cases, the task will trigger ListenableWorker. OnStopped () callback. You should clean up tasks and terminate them cooperatively in case the system shuts down the application. For example, open database and file handles should be closed at this point, or done earlier. In addition, whenever you want to determine whether the task can be terminated the query ListenableWorker. IsStopped (). Even if you indicate that your work is done by returning a result after calling onStopped(), WorkManager ignores this result because the task is considered finished.

Loop task

The advantage of your application is that it needs to run certain tasks periodically. For example, an application might periodically back up data, download new data, or upload logs to the server.

Use PeriodicWorkRequest to perform tasks that need to run periodically.

PeriodicWorkRequest cannot be linked. If the task requires links, consider using OneTimeWorkRequest.

Constraints constraints = new Constraints.Builder()
        .setRequiresCharging(true)
        .build();

PeriodicWorkRequest saveRequest =
        new PeriodicWorkRequest.Builder(SaveImageFileWorker.class, 1, TimeUnit.HOURS)
                  .setConstraints(constraints)
                  .build();

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

The cycle interval is the minimum time for two repeated executions. The actual execution time of the task depends on the constraints of the task setup and the optimization of the system.

You can observe the status of PeriodicWorkRequest in the same way as OneTimeWorkRequest.

The only task

A unique task is a useful concept that guarantees that there can only be one chain of tasks with a particular name at any one time. Unlike ids that are automatically generated by the WorkManager, unique names are readable and specified by the developer. Unlike tag, a unique name can be associated with only one task chain.

By calling the WorkManager. EnqueueUniqueWork () or WorkManager. EnqueueUniqueWork () to create a unique task queue. The first parameter is a unique name — used to identify the WorkRequest. The second parameter is the conflict resolution policy, specifying what the WorkManager should do if an unfinished task chain of the same name already exists:

  • REPLACE: Cancel an existing quest chain and REPLACE it with a new one;
  • KEEP: KEEP existing tasks and discard new task requests;
  • APPEND: Put the new task after the existing task, and then execute the first new task when the existing task is complete. You cannot use the APPEND policy for PeriodicWorkRequest.

Unique tasks are useful when there is a task that does not need to be queued multiple times. For example, if your application needs to synchronize data to the network, you can enroll an event named “sync”, and if a task with that name already exists, the new task should be ignored. Unique task queues are also useful if you need to gradually build up a long chain of tasks. For example, a photo-editing app might ask the user to undo a long list of editing actions. Each undo operation may take a while, but they must be performed in the right order. In this case, the application can create an “undo” chain of tasks and place each new action last.

If you want to create a unique task chain, you can use the WorkManager. BeginUniqueWork () instead of beginWith ().

test

Introduction and Settings

WorkManager provides work-test artifacts to unit test tasks on Android devices.

In order to use the work – the test artifacts, need to be in the build. Add androidTestImplementation rely on gradle.

androidTestImplementation "Androidx. Work: work - testing: 2.3.0 - alpha01"
Copy the code

concept

Work – testing provides a special test mode WorkManager implementation, it is initialized with WorkManagerTestInitHelper.

The work-testing artifact provides a SynchronousExecutor to make it easier to test synchronously without dealing with multithreading, locking, or occupancy.

Edit dependencies in build.gradle

 testImplementation 'junit: junit: 4.12'
 androidTestImplementation 'androidx. Test: runner: 1.2.0'
 androidTestImplementation 'androidx. Test. Espresso: espresso - core: 3.2.0'
 androidTestImplementation 'androidx. Test. Ext: junit: 1.1.1'
 androidTestImplementation "Androidx. Work: work - testing: 2.2.0." "
Copy the code

Unit test class Setup

@Before
public void setup(a) {
	Context context = ApplicationProvider.getApplicationContext();
    Configuration config = new Configuration.Builder()
    // Set log level to Log.DEBUG to
    // make it easier to see why tests failed
    	.setMinimumLoggingLevel(Log.DEBUG)
        // Use a SynchronousExecutor to make it easier to write tests
        .setExecutor(new SynchronousExecutor())
        .build();

    // Initialize WorkManager for instrumentation tests.
    WorkManagerTestInitHelper.initializeTestWorkManager(context, config);
}
Copy the code

Build test

WorkManager has been initialized in test mode and is ready to start testing tasks.

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

    @NonNull
    @Override
    public Result doWork(a) {
        Data input = getInputData();
        if (input.size() == 0) {
            return Result.failure();
        } else {
            returnResult.success(input); }}}Copy the code

Based on the test

Use in test mode is very similar to use in normal applications.

package com.example.hero.workmgr;

import android.content.Context;
import android.util.Log;

import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.work.Configuration;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import androidx.work.testing.SynchronousExecutor;
import androidx.work.testing.WorkManagerTestInitHelper;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

/**
 * Instrumented test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Before
    public void setup(a) {
        Context context = ApplicationProvider.getApplicationContext();
        Configuration config = new Configuration.Builder()
                // Set log level to Log.DEBUG to
                // make it easier to see why tests failed
                .setMinimumLoggingLevel(Log.DEBUG)
                // Use a SynchronousExecutor to make it easier to write tests
                .setExecutor(new SynchronousExecutor())
                .build();

        // Initialize WorkManager for instrumentation tests.
        WorkManagerTestInitHelper.initializeTestWorkManager(context, config);
    }

    @Test
    public void testWorker(a) throws Exception {
        Data input = new Data.Builder().put("a".1).put("b".2).build();

        OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(TestWorker.class).setInputData(input).build();

        WorkManager mgr = WorkManager.getInstance(ApplicationProvider.getApplicationContext());
        mgr.enqueue(request).getResult().get();
		// This interface returns a StatusRunnable, which calls settableFuture.set () when the WorkInfo is retrieved from the database, and gets () returns the corresponding WorkInfo
        WorkInfo workInfo = mgr.getWorkInfoById(request.getId()).get();
        assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED));
        workInfo = mgr.getWorkInfoById(request.getId()).get();
        assertThat(workInfo.getState(), is(WorkInfo.State.RUNNING));
        workInfo = mgr.getWorkInfoById(request.getId()).get();
        assertThat(workInfo.getState(), is(WorkInfo.State.SUCCEEDED));
        Data output = workInfo.getOutputData();
        assertThat(output.getInt("a", -1), is(1)); }}Copy the code

Simulate constraint, delay, and loop tasks

WorkManagerTestInitHelper provides a TestDriver instance, it can simulate the initialization delay ListenableWorker need the constraint conditions and loop task cycle, etc.

Test initialization delay

Tasks can set the initialization delay. Using TestDriver to set the initialization delay required by the task, you don’t need to wait for that time to arrive, so you can test whether the delay is valid.

@Test
public void testDelay(a) throws Exception {
    Data input = new Data.Builder().put("a".1).put("b".2).build();

    OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(TestWorker.class).setInputData(input).setInitialDelay(10, TimeUnit.SECONDS).build();
    WorkManager mgr = WorkManager.getInstance(ApplicationProvider.getApplicationContext());

    TestDriver driver = WorkManagerTestInitHelper.getTestDriver(ApplicationProvider.getApplicationContext());
    mgr.enqueue(request).getResult().get();

    driver.setInitialDelayMet(request.getId());
    WorkInfo workInfo = mgr.getWorkInfoById(request.getId()).get();
    assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED));
    workInfo = mgr.getWorkInfoById(request.getId()).get();
    assertThat(workInfo.getState(), is(WorkInfo.State.RUNNING));
    workInfo = mgr.getWorkInfoById(request.getId()).get();
    assertThat(workInfo.getState(), is(WorkInfo.State.SUCCEEDED));
}
Copy the code
Test the constraint

TestDriver can call setAllConstraintsMet to set all constraints satisfied.

@Test
public void testConstraint(a) throws Exception {
    Data input = new Data.Builder().put("a".1).put("b".2).build();

    Constraints constraints = new Constraints.Builder().setRequiresDeviceIdle(true).build();
    OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(TestWorker.class).setInputData(input).setConstraints(constraints).build();
    WorkManager mgr = WorkManager.getInstance(ApplicationProvider.getApplicationContext());

    TestDriver driver = WorkManagerTestInitHelper.getTestDriver(ApplicationProvider.getApplicationContext());
        mgr.enqueue(request).getResult().get();

    driver.setAllConstraintsMet(request.getId());
    WorkInfo workInfo = mgr.getWorkInfoById(request.getId()).get();
    assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED));
    workInfo = mgr.getWorkInfoById(request.getId()).get();
    assertThat(workInfo.getState(), is(WorkInfo.State.RUNNING));
    workInfo = mgr.getWorkInfoById(request.getId()).get();
    assertThat(workInfo.getState(), is(WorkInfo.State.SUCCEEDED));
}
Copy the code
Test loop task

TestDriver provides a setPeriodDelayMet to indicate that the interval has been reached.

@Test
public void testPeriod(a) throws Exception {
    Data input = new Data.Builder().put("a".1).put("b".2).build();

    PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(TestWorker.class, 10, TimeUnit.SECONDS).setInputData(input).build();
    WorkManager mgr = WorkManager.getInstance(ApplicationProvider.getApplicationContext());

    TestDriver driver = WorkManagerTestInitHelper.getTestDriver(ApplicationProvider.getApplicationContext());
    mgr.enqueue(request).getResult().get();

    driver.setPeriodDelayMet(request.getId());
    WorkInfo workInfo = mgr.getWorkInfoById(request.getId()).get();
    assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED));
    workInfo = mgr.getWorkInfoById(request.getId()).get();
    assertThat(workInfo.getState(), is(WorkInfo.State.RUNNING));
    workInfo = mgr.getWorkInfoById(request.getId()).get();
    // After the loop completes, the state will still be ENQUEUED (logic for handleResult() in WorkerWrapper)
    assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED));
}
Copy the code

Use WorkManager 2.1.0 for testing

Starting with version 2.1.0, WorkManager provides a new API that makes it easier to test workers, ListenableWorker, and ListenableWorker variants (CoroutineWorker and RxWorker).

Before, in order to test task, you need to use WorkManagerTestInitHelper to initialize the WorkManager. In 2.1.0, it is not necessary to use it. If only for the business logic of the test task, no longer need to use WorkManagerTestInitHelper.

Test ListenableWorker and its variants

To test the ListenableWorker and its variants, you can use TestListenableWorkerBuilder. The builder can create an instance of the ListenableWorker to test the business logic in the task.

package com.example.hero.workmgr;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;

import androidx.annotation.NonNull;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.work.ListenableWorker;
import androidx.work.WorkerParameters;

import com.google.common.util.concurrent.ListenableFuture;

public class SleepWorker extends ListenableWorker {
    private ResolvableFuture<Result> mResult;
    private Handler mHandler;
    private final Object mLock;
    private Runnable mRunnable;

    public SleepWorker(@NonNull Context appContext, @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
        mResult = ResolvableFuture.create();
        mHandler = new Handler(Looper.getMainLooper());
        mLock = new Object();
    }

    @NonNull
    @Override
    public ListenableFuture<Result> startWork(a) {
        mRunnable = new Runnable() {
            @Override
            public void run(a) {
                synchronized(mLock) { mResult.set(Result.success()); }}}; mHandler.postDelayed(mRunnable,1000L);
        return mResult;
    }

    @Override
    public void onStopped(a) {
        super.onStopped();
        if(mRunnable ! =null) {
            mHandler.removeCallbacks(mRunnable);
        }
        synchronized (mLock) {
            if(! mResult.isDone()) { mResult.set(Result.failure()); }}}}Copy the code

To test the SleepWorker, use TestListenableWorkerBuilder first creates an instance of the Worker. The creator can also be used to set parameters such as label, input, and number of attempts.

@Test
public void testSleepWorker(a) throws Exception{
    // Create a worker instance directly and call its methods
    ListenableWorker worker = TestListenableWorkerBuilder.from(ApplicationProvider.getApplicationContext(), SleepWorker.class).build();
    ListenableWorker.Result result = worker.startWork().get();
    assertThat(result, is(ListenableWorker.Result.success()));
}
Copy the code

Testing tasks

One task is as follows:

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

    @NonNull
    @Override
    public Result doWork(a) {
        try {
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        returnResult.success(); }}Copy the code

Use TestWorkerBuilder for testing. TestWorkerBuilder allows you to specify thread pools to run tasks.

@Test
public void testThreadSleepWorker(a) throws Exception {
    Sleep woker = (Sleep) TestWorkerBuilder.from(ApplicationProvider.getApplicationContext(), Sleep.class,
            Executors.newSingleThreadExecutor()).build();
    ListenableWorker.Result result = woker.doWork();
    assertThat(result, is(ListenableWorker.Result.success()));
}
Copy the code