阅读 2921

Jetpack-WorkManager

WorkManager的主要特点

  • 向后兼容到API14
    • API 23以上使用JobScheduler
    • 在API 14~22之间使用BroadcastReceiver和AlarmManager的组合
  • 可以增加任务的约束,如网络或者充电状态
  • 可以调度一次性的或者周期性的异步任务
  • 可以监测和管理需要调度的任务
  • 可以把任务链接在一起
  • 保证任务执行,即使app或者设备被重启
  • 遵守节电功能如Doze模式

WorkManager是为了那些可延后执行的任务而设计,这些任务不需要立即执行,但是需要保证任务能被执行,即使应用退出或者设备重启。例如:

  • 向后台服务发送日志或者分析
  • 周期性地与服务器同步数据

WorkManager不是为某些进程内的后台任务设计的,这些任务会在app进程退出时被停止,也不是那些需要立即执行的任务。

使用WorkManager

声明依赖

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"
  }
复制代码

创建后台任务

继承Worker,并重写doWork()

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

    @NonNull
    @Override
    public Result doWork() {
        //business logic
        return Result.success();
    }
}
复制代码

Result返回结果有三种:

  • 执行成功,Result.success()或Result.success(data)
  • 执行失败,Result.failure()或Result.failure(data)
  • 需要重试,Result.retry()

配置执行任务

Worker定义了具体的任务,WorkRequest定义了如何执行以及何时执行任务。如果是一次性的任务,可以用O呢TimeWorkRequest,如果是周期性的任务,可以使用PeriodicWorkRequest。

OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class).build();
复制代码
PeriodicWorkRequest periodicWorkRequest = new PeriodicWorkRequest.Builder(UploadWorker.class, 10, TimeUnit.SECONDS).build();
复制代码

调度WorkRequest

调用WorkManager的enqueue方法

WorkManager.getInstance(ctx).enqueue(uploadReq);
复制代码

任务的具体执行时机依赖于WorkRequest设置的约束,以及系统的优化。

定义WorkRequest

通过自定义WorkRequest可以解决以下场景:

  • 给任务增加约束条件,如网络状态
  • 保证任务执行的最低延迟时间
  • 处理任务的重试和补偿
  • 处理任务的输入和输出
  • 给一组任务设置标签

任务的约束

给任务增加约束,表示什么时候该任务能执行。

例如,可以指定任务只有在设备空闲或者连接到电源时才能执行。

Constraints constraints = new Constraints.Builder()
				//当本地的contenturi更新时,会触发任务执行(api需大于等于24,配合JobSchedule)
                .addContentUriTrigger(Uri.EMPTY, true)
    			//当content uri变更时,执行任务的最大延迟,配合JobSchedule
                .setTriggerContentMaxDelay(10, TimeUnit.SECONDS)
    			//当content uri更新时,执行任务的延迟(api>=26)
                .setTriggerContentUpdateDelay(100, TimeUnit.SECONDS)
    			//任务的网络状态:无网络要求,有网络连接,不限量网络,非移动网络,按流量计费的网络
                .setRequiredNetworkType(NetworkType.NOT_ROAMING)
    			//电量足够才能执行
                .setRequiresBatteryNotLow(true)
    			//充电时才能执行
                .setRequiresCharging(false)
    			//存储空间足够才能执行
    			.setRequiresStorageNotLow(false)
    			//设备空闲才能执行
                .setRequiresDeviceIdle(true)
                .build();
复制代码

当设置了多个约束,只有这些条件都满足时,任务才会执行。

当任务在运行时,如果约束条件不满足,WorkManager会终止任务。这些任务会在下一次约束条件满足时重试。

延迟初始化

如果任务没有约束或者约束条件满足时,系统可能会立刻执行这些任务。如果不希望任务立即执行,可以指定这些任务延迟一定时间再执行。

OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class).setInitialDelay(10, TimeUnit.SECONDS).build();
复制代码

重试和补偿策略

如果需要WorkManager重试任务,可以让任务返回Result.retry()。

任务会被重新调度,并且会有一个默认的补偿延迟和策略。补偿延迟指定了任务被重试的一个最小的等待时间。补充策略定义了补偿延迟在接下来的几次重试中会如何增加。默认是指数增加的。

OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class).setInitialDelay(10, TimeUnit.SECONDS)
                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10 ,TimeUnit.SECONDS)
                .build();
复制代码

定义输入和输出

任务可能需要传入数据作为输入参数或者返回结果数据。例如,一个上传图片的任务需要图片的URI,可能也需要图片上传后的地址。

输入和输出的值以键-值对的形式存储在Data对象中。

Data data = new Data.Builder().putString("key1", "a").build();
OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class)
                .setInputData(data)
                .build();
复制代码

Wroker类调用Worker.getInputData()来获取输入参数。

Data类也可以作为输出。在Worker中返回Data对象,通过调用Result.success(data)或Result.failure(data)。

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

    @NonNull
    @Override
    public Result doWork() {
        //business logic
        Data data = new Data.Builder().putString("image-url","http://xxxx.png").build();
        return Result.success(data);
    }
}
复制代码

标记任务

对任何的WorkRequest对象,通过给一组任务赋值一个标签就可以在逻辑上把它们变成一个组。这样就可以操作特定标签的全部任务。

例如,WorkManager.cancelAllWorkByTag(String)取消了所有该标签的任务;WorkManager.getWorkInfosByTagLiveData(String)返回了一个LiveData包含了该标签下的全部任务的状态列表

OneTimeWorkRequest uploadReq = new OneTimeWorkRequest.Builder(UploadWorker.class)
                .addTag("upload")
                .build();
复制代码

任务的状态和观察任务

任务状态

在任务的生命周期中,会经过各种状态:

  • BLOCKED,当任务的先决条件还未满足时,任务处于阻塞状态
  • ENQUEUED,当任务的约束条件和时间满足能够执行时,处于入队状态
  • RUNNING,当任务正在被执行
  • SUCCEEDED,一个任务返回Result.success(),就处于成功状态。这个是终点状态;只有一次性的任务(OneTimeWorkRequest)能到达这个状态
  • FAILED,一个任务返回Result.failure(),就处于失败状态。这也是一个终点状态;只有一次性的任务(OneTimeWorkRequest)能到达这个状态。所有依赖它的任务都被会标记为FAILED并且不会被执行
  • CANCELLED,当显式地取消一个没有终止的WorkRequest,会处于取消状态。所有依赖它的任务也会被标记为CANCELLED,并且不会执行

观察任务

当把任务放入队列中,WorkManager允许检查它们的状态。这些信息可以通过WorkInfo对象获取,包含了任务的id,tag,当前的State和输出的数据。

有以下几个方法获取WorkInfo:

  • 对特定的WorkRequest,可以通过id获取它的WorkInfo,调用WorkManager.getWorkInfoById(id)或WorkManager.getWorkInfoByIdLiveData(id)
  • 对一个给定的tag,可以获取所有匹配这个tag的任务们的WorkInfo对象,调用WorkManager.getWorkInfosByTag(tag)或WorkManager.getWorkInfosByTagLiveData(tag)
  • 对一个独特的任务的名称,可以获取所有符合的任务的WorkInfo对象,调用WorkManager.getWorkInfosForUniqueWork(name)或WorkManager.getWorkInfosForUniqueWorkLiveData(name)

上述方法返回的LiveData可以通过注册一个监听器观察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) {
                
            }
        });
复制代码

观察任务的进度

2.3.0-alpha01版本的WorkManager增加了设置和观察任务的进度的支持。如果应用在前台时任务在运行,进度信息可以展示给用户,通过API返回的WorkInfo的LiveData。

ListenableWorker现在支持setProgressAsync(),能够保存中间进度。这些API使得开发者能够设置进度,以便在UI上能够展示出来。进度用Data类型表示,这是一个可序列化的属性容器(类似输入和输出,受同样的限制)。

进度信息只有在ListenableWorker运行时才能被观察和更新。当ListenableWorker结束时设置进度会被忽略。通过调用getWorkInfoBy..()或者getWorkInfoBy...LiveData()接口来观察进度信息。这些方法能返回WorkInfo的对象实例,它们有一个新的getProgress()方法能返回Data对象。

更新进度

开发者使用ListenableWorker或者Worker,setProgressAsync()接口会返回一个ListenableFuture;更新进度是异步的,包含了存储进度信息到数据库。在Kotlin中,可以使用CoroutineWorker对象的setProgress()扩展方法来更新进度信息。

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() {
        setProgressAsync(new Data.Builder().putInt("progress", 100).build());
        return Result.success();
    }
}
复制代码

观察进度

观察进度信息比较简单。可以使用getWorkInfoBy...()或getWorkInfoBy...LiveData()方法,获取一个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);
            }
        });
复制代码

链接工作

简介

WorkManager允许创建和入队一连串的任务,可以指定多个依赖的任务,以及它们的执行顺序。如果要以一个特定的顺序执行多个任务时会非常有用。

要创建一连串的任务,可以使用WorkManager.beginWith(OneTimeWorkRequest)或者WorkManager.beginWith(List),它们会返回一个WorkContinuation实例。

一个WorkContinuation实例之后可以用来添加依赖的OneTimeWorkRequest,通过调用WorkContainuation.then(OneTimeWorkRequest)或WorkContinuation.then(List)。

每个WorkContinuation.then(...)的调用,会返回一个新的WorkContinuation实例。如果添加了OneTimeRequest的列表,这些请求有可能会串行地运行。

最终,可以用WorkContinuation.enqueue()方法把WorkContinuation链放入队列。

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();
复制代码

输入合并

当使用链式的OneTimeWorkRequest,父OneTimeWorkRequest的输出会作为子任务的输入。所以上例中的filter1,filter2和filter3的输出会作为compress任务的输入。

为了管理来自多个父任务的输入,WorkManager使用InputMerger进行输入合并。

WorkManager提供了两种不同类型的InputMerger:

  • OverwritingInputMerger试图把所有输入中的键添加到输出中。当键冲突时,会覆盖之前的键。
  • ArrayCreatingInputMerger在必要时会试图合并所有输入,放入数组中。
OneTimeWorkRequest compress =
    new OneTimeWorkRequest.Builder(CompressWorker.class)
        .setInputMerger(ArrayCreatingInputMerger.class)
        .setConstraints(constraints)
        .build();
复制代码

链接和任务的状态

当创建一个OneTimeWorkRequest任务链时,有几件事要记住:

  • 当所有父OneTimeWorkRequest成功执行时,子OneTimeWorkRequest才会是非阻塞的(过渡到ENQUEUED状态)。
  • 当任何一个父OneTimeWorkRequest执行失败,所有依赖于它的OneTimeWorkRequest都是被标记为FAILED。
  • 当任何一个父OneTimeWorkRequest被取消,所有依赖于它的OneTimeWorkRequest都会被标记为CANCELED。

取消和终止任务

如果不再需要入队的任务执行,可以取消它。取消一个单独的WorkRequest最简单的方法是使用id并调用WorkManager.cancenWorkById(UUID)。

WorkManager.cancelWorkById(workRequest.getId());
复制代码

在底层,WorkManager会检查任务的状态。如果这个任务已经完成,没有任何事情发生。否则,这个任务的状态会转移到CANCELED 并且这个任务以后不会再运行。任何依赖这个任务的其他WorkRequest都会被标记为CANCELED。

另外,如果当前任务正在运行,这个任务会触发ListenableWorker.onStopped()的回调。重写这个方法来处理任何可能的清理工作。

也可以用标签来取消任务,通过调用WorkManager.cancelAllWorkByTag(String)。注意,这个方法会取消所有有这个标签的任务。另外,也可以调用WorkManager.cancelUniqueWork(String)取消带有该独特名字的全部任务。

终止一个运行中的任务

有几种情况,运行中的任务会被WorkManager终止:

  • 显式地调用了取消任务的方法
  • 任务的约束条件再也不会满足
  • 系统因为某些原因终止了应用。如果超过了执行的最后时间10分钟以上就有可能发生。这个任务之后会被调度进行重试。

在这些情况下,任务会触发ListenableWorker.onStopped()的回调。你应该执行任务清理和配合地终止任务,以防系统会关闭应用。比如,在此时应该关闭开启的数据库和文件句柄,或者在更早的时间里做这些事情。另外,无论何时想要判断任务是否被终止了可以查询ListenableWorker.isStopped()。即使您通过在调用onStopped()之后返回一个结果来表示您的工作已经完成,WorkManager也会忽略这个结果,因为这个任务已经被认为是结束了。

循环任务

你的应用优势会需要周期性地运行某些任务。比如,应用可能会周期性地备份数据,下载新的数据,或者上传到日志到服务器。

使用PeriodicWorkRequest来执行那些需要周期性地运行的任务。

PeriodicWorkRequest不能被链接。如果任务需要链接,考虑使用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);
复制代码

周期间隔是两次重复执行的最小时间。任务实际执行的时间取决于任务设置的约束和系统的优化。

观察PeriodicWorkRequest的状态的方法跟OneTimeWorkRequest一样。

唯一任务

唯一任务是一个有用的概念,它保证了某一时刻只能有一个带有特定名称的任务链。不像id是由WorkManager自动生成的,唯一名称是可读的,并且是开发者指定的。也不像tag,唯一名称只能跟一个任务链关联。

可以通过调用WorkManager.enqueueUniqueWork()或者WorkManager.enqueueUniqueWork()来创建一个唯一任务队列。第一个参数是唯一名字—用于识别WorkRequest。第二个参数是冲突的解决策略,指定了如果已经存在一个未完成的同名任务链时WorkManager采取的措施:

  • REPLACE:取消已经存在的任务链,并用新的取代;
  • KEEP:保持已有的任务,并放弃新的任务请求;
  • APPEND:把新的任务放在已有的任务后,当已有的任务完成后再执行新加入的第一个任务。对于PeriodicWorkRequest,不能用APPEND策略。

如果有一个任务不需要多次放入队列时,唯一任务会很有用。例如,如果你的应用需要同步数据到网络,可以入队一个命名为“sync”的事件,并且如果已经有这个名字的任务了,那么新的任务应该被忽略。如果你需要逐渐地建立一个很长的任务链,唯一任务队列也很有用。例如,一个相片编辑应用可能会让用户撤销一长串编辑动作。每个撤销操作可能会耗时一段时间,但是它们必须按正确的顺序执行。在这个情况下,这个应用可以创建一个“undo”的任务链,并把每个新的操作放在最后。

如果要创建一个唯一任务链,可以使用WorkManager.beginUniqueWork()而不是beginWith()。

测试

介绍和设置

WorkManager提供了work-test工件在Android设备上为任务进行单元测试。

为了使用work-test工件,需要在build.gradle中添加androidTestImplementation依赖。

androidTestImplementation "androidx.work:work-testing:2.3.0-alpha01"
复制代码

概念

work-testing提供了一个测试模式下的WorkManager的特殊实现,它是用WorkManagerTestInitHelper进行初始化。

work-testing工件提供了一个SynchronousExecutor使得能更简单地用同步方式进行测试,不需要去处理多线程,锁或占用。

在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"
复制代码

单元测试类setup

@Before
public void setup() {
	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);
}
复制代码

构建测试

WorkManager在测试模式下已经初始化,可以开始测试任务。

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

    @NonNull
    @Override
    public Result doWork() {
        Data input = getInputData();
        if (input.size() == 0) {
            return Result.failure();
        } else {
            return Result.success(input);
        }
    }
}
复制代码

基础测试

测试模式下的使用跟正常应用中使用十分类似。

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() {
        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() 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();
		//该接口其实得到的是一个StatusRunnable,从数据库中查询到WorkInfo后会调用SettableFuture.set(),然后get()会返回对应的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));
    }
}
复制代码

模拟约束,延迟和循环任务

WorkManagerTestInitHelper提供一个TestDriver实例,它能够模拟初始化延迟,ListenableWorker需要的约束条件和循环任务的周期等。

测试初始化延迟

任务可以设置初始化延迟。用TestDriver设置任务所需要的初始化延迟,就不需要等待这个时间到来,这样可以测试任务的延迟是否有效。

@Test
public void testDelay() 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));
}
复制代码
测试约束

TestDriver可以调用setAllConstraintsMet设置所有的约束都满足条件。

@Test
public void testConstraint() 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));
}
复制代码
测试循环任务

TestDriver提供了一个setPeriodDelayMet来表示间隔已经达到。

@Test
public void testPeriod() 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();
    //循环任务完成后,状态仍会变成ENQUEUED(WorkerWrapper中的handleResult()的逻辑)
    assertThat(workInfo.getState(), is(WorkInfo.State.ENQUEUED));
}
复制代码

使用WorkManager 2.1.0进行测试

从2.1.0版本开始,WorkManager提供了新的API,能更方便的测试Worker,ListenableWorker,以及ListenableWorker的变体(CoroutineWorker 和RxWorker)。

之前,为了测试任务,需要使用WorkManagerTestInitHelper来初始化WorkManager。在2.1.0中,不一定要使用它。如果只是为了测试任务中的业务逻辑,再也不需要使用WorkManagerTestInitHelper。

测试ListenableWorker和它的变体

为了测试ListenableWorker和它的变体,可以使用TestListenableWorkerBuilder。这个建造器可以创建一个ListenableWorker的实例,用来测试任务中的业务逻辑。

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() {
        mRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    mResult.set(Result.success());
                }
            }
        };

        mHandler.postDelayed(mRunnable, 1000L);
        return mResult;
    }

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

复制代码

为了测试SleepWorker,先用TestListenableWorkerBuilder创建了一个Worker的实例。这个创建器也可以用来设置标签,输入和尝试运行次数等参数。

@Test
public void testSleepWorker() throws Exception{
    //直接创建了一个worker实例,调用它的方法
    ListenableWorker worker = TestListenableWorkerBuilder.from(ApplicationProvider.getApplicationContext(), SleepWorker.class).build();
    ListenableWorker.Result result = worker.startWork().get();
    assertThat(result, is(ListenableWorker.Result.success()));
}
复制代码

测试任务

有一个任务如下:

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

    @NonNull
    @Override
    public Result doWork() {
        try {
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        return Result.success();
    }
}
复制代码

使用TestWorkerBuilder进行测试。TestWorkerBuilder允许指定运行任务的线程池。

@Test
public void testThreadSleepWorker() 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()));
}
复制代码
文章分类
Android
文章标签