WorkManagerを使ったバックグラウンドでのAPI呼び出し

WorkManagerを使ったバックグラウンドでのAPI呼び出し

はじめに

こんにちは。WEAR部Androidチームの半澤です。普段は、「ファッションコーディネートアプリ WEAR」のAndroidアプリ開発を担当しております。

今回は、WorkManagerを使ったバックグラウンドでのAPI呼び出しについて紹介いたします。WorkManagerは時間がかかる処理や永続的な処理などをバックグラウンドで実行するために推奨されるソリューションです。例えばサイズの大きいデータのアップロード処理や定期的なタスクをバックグラウンドで実行したいといったケースで利用されます。

背景

「WorkManagerを使ってアプリのプロセスの状況に依存せずAPI通信をしたい」というのが今回の背景となります。RetrofitなどのAPI通信を簡単に行うためのライブラリなどがありますが、画面遷移のタイミングで通信処理をキャンセルさせるといった仕組みを導入していることも多いかと思います。今回、こういったアプリのライフサイクルの状況に依存させずAPI通信をしたいケースと遭遇し、WorkManagerを利用したところシンプルに実現できました。

概略を説明すると別画面へ遷移するボタンの押下後にAPIを呼び出す必要がありましたが、ボタン押下後は画面を破棄するため、現状のままでは通信処理がキャンセルされてしまうという課題がありました。

基本的な登場人物とアクセス方法

WorkManagerの基本的な処理の流れは以下のとおりです。

  1. Requestを作成
  2. WorkManagerに処理の実行を依頼

Requestの作成方法として下記の2つの実装が標準で提供されています。

  • OneTimeWorkRequest
  • PeriodicWorkRequest

OneTimeWorkRequestはその名の通り、1度のみの処理のスケジュールを設定します。PeriodicWorkRequestは一定間隔で繰り返すようなスケジュールを設定する場合に適してます。今回は失敗した場合のリカバリーを考慮せず、1度のみの送信処理が実行できれば良いという要件としたため、OneTimeWorkRequestを採用しました。このRequestが処理を実行するキューとして積まれ、WorkManagerがRequestをスケジュール実行します。また、今回は特に設定していませんが、Wi-Fiに接続したときやバッテリーが十分あるときに処理を実行する制約を追加で指定できます。

Workerにデータを渡す

はじめに、WorkerはKotlin Coroutinesを使ったCoroutineWorkerを利用しています。本記事で説明しているバージョンは2.6.0です。

ここからは実際のコードを例に挙げて説明していきます。Workerとのデータのやり取りはDataで行います。このクラスはデータをMapで保持しています。基本的なやりとりとしてOneTimeWorkRequestBuilderで用意されているsetInputData()にデータを渡します。また、workDataOfはBuilder処理をラップした拡張関数です。

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

そして、doWork()内で必要な処理を記述します。以下の例ではWorkerのinputDataからrequest時に渡した引数を取得して処理を実行します。

override suspend fun doWork(): Result = withContext(Dispatchers.Default) {
    launch {
        val someData = inputData.getString(IN_KEY_DATA) ?: error("invalid someData")
        //~~ 何らかの処理を実行 ~~//
    }
    return@withContext Result.success()
}

呼び出し元はWorkManagerへrequestのenqueueを依頼し、処理を実行します。

val request = MyWorker.createRequest(someData)
WorkManager.getInstance(context)
            .beginWith(request)
            .enqueue()

基本的なWorkManagerでの処理実行に関しては以上で、特に複雑な処理などはありません。

プリミティブ型以外のデータをWorkerに渡す

今回は実装の見通しを良くしたいという目的から、データクラスをJSON文字列として渡す方法もあるという一例をあわせて紹介します。例えば、Workerで実行されるAPIに次のようなデータ型を含んだパラメータを渡したいといったケースがあったとします。

data class SomeApiParameter(
    val someData: String,
    val someId: Int,
    val somePayload: Payload
)

このpayloadは以下のようなパラメータを渡すとします。

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

    data class Item2(
        val id: Long,
    )
}

上記のようなデータ型を上の例で示したものと同じくRequestにsetInputData(workDataOf(...))で渡します。ここでworkDataOfを見てみると引数がPair<String, Any?>なので、なんでも受け付けてくれるかのようにみえます。

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()
}

しかし、workDataOfの実装の中身を見てみると実はプリミティブ型以外の型は受け付けておらず、これ以外の型を渡すと例外でクラッシュすることがわかります。

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;
        }

このままではAPIのパラメータであるデータ型を渡すことはできません。これをどのように解決したかというとRequest時にデータ型をJSON文字列に変換し渡すことで解決しました。

fun createRequest(
    someData: String,
    someId: Int,
    somePayload: Payload,
): OneTimeWorkRequest {
    val jsonStr: String = gson.toJson(somePayload) //JSON文字列へ
    return OneTimeWorkRequestBuilder<MyWorker>()
        .setInputData(workDataOf(
            IN_KEY_DATA to someData,
            IN_KEY_ID to someId,
            IN_KEY_PAYLOAD to jsonStr,
        ))
        .setBackoffCriteria(
            BackoffPolicy.LINEAR,
            OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
            TimeUnit.MILLISECONDS)
        .build()
}

WorkerにJSON文字列としてpayloadを渡し、実行時はSomePayloadクラスに再度戻すことでデータ型への対応も問題なくできました。

override suspend fun doWork(): Result = withContext(Dispatchers.Default) {
        launch {
            val someData = inputData.getString(IN_KEY_DATA) ?: error("invalid someData")
            val someId = inputData.getInteger(IN_KEY_ID) ?: error("invalid someId")
            val jsonStr = inputData.getString(IN_KEY_PAYLOAD) ?: error("invalid jsonStr")
            val payload = gson.fromJson(jsonStr, SomePayload::class.java) // データ型に戻す
            client.postAPI(SomeApiParameter(someData, someId, payload))
        }
        return@withContext Result.success()
    }

今回の例では、WorkManagerでの処理結果は無視していますが、成功や失敗を通知したい場合は以下の例のようにworkDataOfの結果をData型で返してあげれば良さそうです。また、処理経過をハンドリングしたい場合にはWorkQueryを使って実行されている処理をモニタリングするのも良さそうです。

response = client.postAPI(SomeApiParameter(someData, someId, payload))

if (response.isSuccessful) {
    val outputData = workDataOf(OUT_KEY_JSON_STRING to gson.toJson(response?.body()))
    return@withContext Result.success(outputData)
} else {
    val outputData = workDataOf(OUT_KEY_JSON_STRING to gson.toJson(response?.body()))
    return@withContext Result.failure(outputData)
}

さいごに

今回紹介した事例で、WorkManagerを使ったバックグラウンドでのAPI通信について解説させていただきました。バックグラウンドで処理を実行するのであればServiceクラスなどもありますが、WorkManagerを使うとよりシンプルにやりたいことが実現できたと思います。

さいごに、ZOZOでは、一緒にモダンなサービス作りをしてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!

corp.zozo.com

もちろんAndroidエンジニアの採用も積極的に行っています。ご興味のある方は、以下のリンクからぜひご応募ください!

hrmos.co

カテゴリー