Jetpack-WorkManager

WorkManager的主要特色

  • 向後兼容到API14
    • API 23以上使用JobScheduler
    • 在API 14~22之間使用BroadcastReceiver和AlarmManager的組合
  • 能夠增長任務的約束,如網絡或者充電狀態
  • 能夠調度一次性的或者週期性的異步任務
  • 能夠監測和管理須要調度的任務
  • 能夠把任務連接在一塊兒
  • 保證任務執行,即便app或者設備被重啓
  • 遵照節電功能如Doze模式

WorkManager是爲了那些可延後執行的任務而設計,這些任務不須要當即執行,可是須要保證任務能被執行,即便應用退出或者設備重啓。例如:java

  • 向後臺服務發送日誌或者分析
  • 週期性地與服務器同步數據

WorkManager不是爲某些進程內的後臺任務設計的,這些任務會在app進程退出時被中止,也不是那些須要當即執行的任務。android

使用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返回結果有三種:api

  • 執行成功,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方法bash

WorkManager.getInstance(ctx).enqueue(uploadReq);
複製代碼

任務的具體執行時機依賴於WorkRequest設置的約束,以及系統的優化。服務器

定義WorkRequest

經過自定義WorkRequest能夠解決如下場景:網絡

  • 給任務增長約束條件,如網絡狀態
  • 保證任務執行的最低延遲時間
  • 處理任務的重試和補償
  • 處理任務的輸入和輸出
  • 給一組任務設置標籤

任務的約束

給任務增長約束,表示何時該任務能執行。多線程

例如,能夠指定任務只有在設備空閒或者鏈接到電源時才能執行。app

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()));
}
複製代碼
本站公眾號
   歡迎關注本站公眾號,獲取更多信息