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