WorkManagerを使ってバックグラウンドでエンドポイントを呼び出す

## はじめに
### WorkManagerとは
WorkManagerは重たい処理や永続的な処理などをバックグラウンドで実行するために提供されているAndroidのフレームワークです。例えば重いデータのアップロード処理や定期的なタスクをバックグラウンドで実行したいといったユースケースで推奨されるフレームワークです。

### やりたいこと
今回はViewのライフサイクルに依存せずにエンドポイントを呼びたいケースがあり、WorkManagerを使ったやり方でシンプルに実現できたため、その説明したいと思います。簡単に説明するとボタンを押下後に、エンドポイントを呼び出す必要があったのですが呼び出すViewが破棄されてしまうため通常のやり方だと非同期処理がキャンセルしてしまうのため、WorkManagerを使ってライフサイクルに依存せずにエンドポイントを呼び出したいというのがモチベーションです。

## アクセス方法
Workerには２種類のアクセス方法があり、

 - OneTimeWorkRequest
 - PeriodicWorkRequest 

が実装されており、OneTimeWorkRequestはその名の通り1回のみの処理のスケジュール設定をし、PeriodicWorkRequestは一定間隔で繰り返すようなスケジュール設定をする場合に適してます。今回に関してはエンドポイントの呼び出しということでOneTimeWorkRequestを採用しました。その他、今回では特に設定はしておりませんが、WiFiに接続したときやバッテリーが十分なときに処理を実行する[制約](https://developer.android.com/topic/libraries/architecture/workmanager/how-to/define-work?hl=ja#work-constraints)も指定できるようです。

## 実際にWorkerにデータを渡す
実際のWorkerとのデータのやり取りは[Data](https://developer.android.com/reference/androidx/work/Data)で行います。このクラスはデータをMapで保持してます。基本的なやりとりとしてはOneTimeWorkRequestBuilderで用意されているsetInputData()にデータを渡します。workDataOfはBuilder処理をラップした拡張関数です。そして実行時にWorkerのinputDataからrequest時に渡した引数を取得します。

```kotlin
fun createRequest(
    data: String,
): OneTimeWorkRequest {
    return OneTimeWorkRequestBuilder<MyWorker>()
        .setInputData(workDataOf(
            IN_KEY_DATA to data,
        ))
        .setBackoffCriteria(
            BackoffPolicy.LINEAR,
            OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
            TimeUnit.MILLISECONDS)
        .build()
}
```

```kotlin
override suspend fun doWork(): Result = withContext(Dispatchers.Default) {
        launch {
            val data = inputData.getString(IN_KEY_DATA) ?: error("invalid data")
            //~~ 省略 ~~//
        }
        return@withContext Result.success()
    }
```

基本的なやりとりに関しては以上ですが、例えば、Workerで以下のようなパラメータをエンドポイントに渡したいといったケースがあったとします。

```kotlin
data class Parameter(
    val hoge: String,
    val fugaId: Int,
    val payload: Payload
)
```

Payloadは`sealed interface`としてpayloadは以下のようなパラメータを渡すとします。

```kotlin
data class ItemPayload(
    val item1: Item1,
    val item2: Item2,
) : Payload {
    data class Item1(
        val id: Long,
        val itemName: String,
    )

    data class Item2(
        val id: Long,
    )
}
```

上記のようなデータ型を上の例で示したのと同じくリクエスト時にsetInputData(workDataOf(...))で渡します。workDataOfは引数が`Pair<String, Any?>`なのでなんでも受け付けてくれるようにみえますが、workDataOfは実装の中身をみると実はプリミティブ型以外の型は受け付けていないことがわかります。

```kotlin:androidx.work.Data.kt
public 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()
}
```

```kotlin:androidx.work.Data.kt
public 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;
        }
```

ここでエンドポイントの引数であるデータ型をどうWorkerに渡すのかですが、JSON文字列に変換し受け渡すことで解決しました。Worker実行時にはItemPayloadクラスに再度戻すことでデータ型への対応も問題なくエンドポイントのパラメータとして渡すことができます。

```kotlin
fun createRequest(
    hoge: String,
    fugaId: Int,
    payload: Payload,
): OneTimeWorkRequest {
    val jsonStr: String = gson.toJson(payload) //JSON文字列へ
    return OneTimeWorkRequestBuilder<MyWorker>()
        .setInputData(workDataOf(
            IN_KEY_HOGE to hoge,
            IN_KEY_FUGA_ID to fugaId,
            IN_KEY_PAYLOAD to payload,
        ))
        .setBackoffCriteria(
            BackoffPolicy.LINEAR,
            OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
            TimeUnit.MILLISECONDS)
        .build()
}
```

```kotlin
override suspend fun doWork(): Result = withContext(Dispatchers.Default) {
        launch {
            val hoge = inputData.getString(IN_KEY_HOGE) ?: error("invalid hoge")
            val fugaId = inputData.getInteger(IN_KEY_FUGA_ID) ?: error("invalid fugaId")
            val jsonStr = inputData.getString(IN_KEY_PAYLOAD) ?: error("invalid jsonStr")
            val payload = gson.fromJson(jsonStr, ItemPayload::class.java)
            postAPI(hoge, fugaId, payload)
        }
        return@withContext Result.success()
    }
```

また、もしエンドポイントの結果をハンドリングしたい場合は、処理結果をworkDataOfで処理結果をData型で返してあげれば良さそうです。

```kotlin
    result = postAPI(hoge, fugaId, payload)
    val outputData = workDataOf(OUT_KEY_JSON_STRING to gson.toJson(result))
    return@withContext Result.success(outputData)
```

以上です。ライフサイクルに依存しない通信処理ですがWorkManagerを利用すると簡単でかなりシンプルに実装ができる印象でした。