こんにちは、モバイルクライアントグループの中島です。
今回はWorkManagerを使った非同期処理で、WorkManagerとViewModelの間でデータのやりとりを行なう方法について話したいと思います。
ここで「データのやりとり」と言っているのは、WorkManagerに処理をリクエストするViewModelとWorkManager内で実際に処理を行なうWorkerクラス間のデータ受け渡しを指します。
なお、執筆時に利用している WorkManager のバージョンは2.3.0
です。
Studyplus AndroidアプリにおけるWorkManagerの導入については、下記をご参照ください。
やりたいこと
具体的にやりたいことはこんな感じです。
TODO1: ViewModel -> Worker
- Workerで行なう処理のためにパラメータを渡したい
TODO2: Worker -> ViewModel
- Workerで行なった処理の結果をViewModelに返したい
class MyViewModel @Inject constructor( private val workManager: WorkManager ) : ViewModel() { fun request(data: String) { // TODO 1: ここでWorkerにデータを渡したい val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder<MyEventWorker>().build() workManager.enqueue(request) // TODO 2: ここでWorkerの実行結果を処理したい } }
Workerですが、Kotlin Coroutinesを使ったCoroutineWorker
が用意されていますので、Studyplus ではそれを利用しています。
CoroutineWorker
の実行メソッドは suspend function のdoWork()
です。
class MyEventWorker @AssistedInject constructor( @Assisted private val appContext: Context, @Assisted private val params: WorkerParameters ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { // TODO 1': ここでViewModelからのデータを扱いたい return runCatching { // サーバのAPI呼び出しなど }.fold( // TODO 2': ここからViewModelへ結果を返したい onSuccess = { Result.success() }, onFailure = { Result.failure() } ) } }
やり方
公式ドキュメントに倣います。
Workerとのデータのやり取りにはandroidx.work.Data
クラスを用います。
このクラスはデータをMapで保持しています。
ViewModel -> Worker
WorkRequest
のBuilderに用意されているsetInputData(@NonNull Data inputData)
で渡します- Workerの
inputData
から取得します
Worker -> ViewModel
- Resultを返す際に
Data
を渡します getWorkInfoByIdLiveData(request.id)
メソッドを用いてWorkInfo
クラスのLiveDataを取得します
- Resultを返す際に
const val REQUEST_DATA_MAP_KEY = "request_data_map_key" const val RESULT_DATA_MAP_KEY = "result_data_map_key" // 実行結果を受け取るLiveData val workResultLiveData = MediatorLiveData<String>() fun request(data: String) { // TODO 1: ここでWorkerにデータを渡したい -> setInputData(Data) // Data作成 val requestData = workDataOf( REQUEST_DATA_MAP_KEY to data ) val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder<MyEventWorker>() .setInputData(requestData) // Dataを添付 .build() workManager.enqueue(request) // TODO 2: ここでWorkerの実行結果を処理したい -> getWorkInfoByIdLiveData(request.id) workInfoLiveData.addSource(workManager.getWorkInfoByIdLiveData(request.id)) { info -> // 処理が終わった時に処理する場合はisFinished if (info.state.isFinished) { // info.outputDataで Data を受け取れる workResultLiveData.value = info.outputData.getString(RESULT_DATA_MAP_KEY) } } }
override suspend fun doWork(): Result { // TODO 1': ここでViewModelからのデータを扱いたい -> inputData.get~~ val requestData = inputData.getString(REQUEST_DATA_MAP_KEY) return runCatching { // サーバのAPI呼び出しなど }.fold( // TODO 2': ここからViewModelへ結果を返したい -> Result.~~(data) onSuccess = { // Data作成 val resultData = workDataOf( RESULT_DATA_MAP_KEY to "success" ) Result.success(resultData) }, onFailure = { // Data作成 val resultData = workDataOf( RESULT_DATA_MAP_KEY to "failure" ) Result.failure(resultData) } ) }
これでデータの受け渡しを行えます。
さらにやりたいこと
Data
でプリミティブな型以外を受け渡しする
Data
クラスにはBuilder処理をラップした拡張関数である workDataOf
が用意されています。
androidx.work.Data.kt
から抜粋
/** * Converts a list of pairs to a [Data] object. * * If multiple pairs have the same key, the resulting map will contain the value * from the last of those pairs. * * Entries of the map are iterated in the order they were specified. */ inline fun workDataOf(vararg pairs: Pair<String, Any?>): Data { val dataBuilder = Data.Builder() for (pair in pairs) { dataBuilder.put(pair.first, pair.second) } return dataBuilder.build() }
このパラメータを見る限り、 Pair<String, Any?>
で一見なんでも入れられるように見えます。
なので私は最初、通常のデータクラスを入れるコードを書いたのですが実行したところクラッシュしました。
関数内で使われているput()
メソッドについて、Data.Builder
クラス本体の実装を追ってみます。
androidx.work.Data.java
から抜粋
/** * Puts an input key-value pair into the Builder. Valid types are: Boolean, Integer, * Long, Float, Double, String, and array versions of each of those types. * Invalid types throw an {@link IllegalArgumentException}. * * @param key A {@link String} key to add * @param value A nullable {@link Object} value to add of the valid types * @return The {@link Builder} * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public @NonNull Builder put(@NonNull String key, @Nullable Object value) { if (value == null) { mValues.put(key, null); } else { Class<?> valueType = value.getClass(); if (valueType == Boolean.class || valueType == Byte.class || valueType == Integer.class || valueType == Long.class || valueType == Float.class || valueType == Double.class || valueType == String.class || valueType == Boolean[].class || valueType == Byte[].class || valueType == Integer[].class || valueType == Long[].class || valueType == Float[].class || valueType == Double[].class || valueType == String[].class) { mValues.put(key, value); } else if (valueType == boolean[].class) { mValues.put(key, convertPrimitiveBooleanArray((boolean[]) value)); } else if (valueType == byte[].class) { mValues.put(key, convertPrimitiveByteArray((byte[]) value)); } else if (valueType == int[].class) { mValues.put(key, convertPrimitiveIntArray((int[]) value)); } else if (valueType == long[].class) { mValues.put(key, convertPrimitiveLongArray((long[]) value)); } else if (valueType == float[].class) { mValues.put(key, convertPrimitiveFloatArray((float[]) value)); } else if (valueType == double[].class) { mValues.put(key, convertPrimitiveDoubleArray((double[]) value)); } else { throw new IllegalArgumentException( String.format("Key %s has invalid type %s", key, valueType)); } } return this; }
Puts an input key-value pair into the Builder. Valid types are: Boolean, Integer, Long, Float, Double, String, and array versions of each of those types. Invalid types throw an {@link IllegalArgumentException}.
Boolean, Integer, Long, Float, Double, String 及びそれらのArrayのみ受け付けていることがわかります。 ドキュメントを参照すると、こちらにも明記されていますね。
ですので、プリミティブ型以外のデータクラスなどを受け渡したい場合は、JSON文字列にしてStringで受け渡しを行ないました。
Serializable
や Percelable
も受け付けていないのは少々意外でしたが、今後追加されたら便利そうですね。
エラーハンドリングと進捗管理をする
Workerの処理が終わった時に処理を行いたいのであれば、doWork()
の返り値を受け取り、isFinished()
で確認して終了時のみ処理すれば十分です。
ですが、例えばエラー時のみの処理を実装したい、処理の進捗を取得したいなどの場合もあるかと思います。
その際には直接 workInfo.state
で分岐してやりましょう。
進捗処理については 2.3.0-alpha1
から機能が追加されています。
doWork()
内でWorkerのsetProgressAsync(Data)
を呼ぶことで、任意の箇所からData
を渡すことができます。
このData
は WorkInfo.State.RUNNING
ステータスとともに流れてきますので、その分岐の中で受け取ります。
const val REQUEST_DATA_MAP_KEY = "request_data_map_key" const val RESULT_DATA_MAP_KEY = "result_data_map_key" const val PROGRESS_DATA_MAP_KEY = "progress_data_map_key" // 実行結果を受け取るLiveData val workResultLiveData = MediatorLiveData<String>() // 進捗状態を受け取るLiveData val workProgressLiveData = MediatorLiveData<Int>() fun request(data: String) { // Data作成 val requestData = workDataOf( REQUEST_DATA_MAP_KEY to data ) val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder<MyEventWorker>() .setInputData(requestData) // Dataを添付 .build() workManager.enqueue(request) workInfoLiveData.addSource(workManager.getWorkInfoByIdLiveData(request.id)) { info -> // stateで分岐 when (info.state) { WorkInfo.State.RUNNING -> { // 進捗処理、進捗はここで受け取る。info.outputDataではなくinfo.progress workProgressLiveData.value = info.progress.getInt(PROGRESS_DATA_MAP_KEY) }, WorkInfo.State.SUCCEEDED -> { // 成功時処理(isFinishedに含まれる) workResultLiveData.value = info.outputData.getString(RESULT_DATA_MAP_KEY) }, WorkInfo.State.FAILED -> { // エラー時時処理(isFinishedに含まれる) workResultLiveData.value = info.outputData.getString(RESULT_DATA_MAP_KEY) }, WorkInfo.State.CANCELLED -> { // 処理のキャンセル時処理(isFinishedに含まれる) workResultLiveData.value = "CANCELLED" }, else -> { // ここでは説明しませんが、他にENQUEUEDとBLOCKEDがあります } } } }
override suspend fun doWork(): Result { val requestData = inputData.keyValueMap[REQUEST_DATA_MAP_KEY] as? String val progressDataStart = workDataOf( PROGRESS_DATA_MAP_KEY to 0 ) setProgressAsync(progressDataStart) // ~~ 何かしら時間のかかる処理など // 50% val progressDataHalf = workDataOf( PROGRESS_DATA_MAP_KEY to 50 ) setProgressAsync(progressDataHalf) // ~~ val progressDataEnd = workDataOf( PROGRESS_DATA_MAP_KEY to 100 ) setProgressAsync(progressDataEnd) return runCatching { // サーバのAPI呼び出しなど }.fold( onSuccess = { // Data作成 val resultData = workDataOf( RESULT_DATA_MAP_KEY to "success" ) Result.success(resultData) }, onFailure = { // Data作成 val resultData = workDataOf( RESULT_DATA_MAP_KEY to "failure" ) Result.failure(resultData) } ) }
最後に
今回は、WorkManager を使った非同期処理とのデータのやり取りについてお話しいたしました。
WorkManager は主にバックグラウンドの処理に使うものですが、結果をViewに反映したいケースなどもカバーされておりとても便利に使えるものだと思います。 今後もリリース動向にも注目していきたいですね。