Jetpack-WorkManager

夕陽下的奔跑發表於2019-09-26

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()));
}
複製程式碼