【kotlin】コールバックパターンの処理をsuspend関数に変換し、直列的に扱う

非同期処理を扱う手法の一つとしてコールバックパターンがあります。
時間のかかる処理を非同期で実行し、完了後に行う後続処理を予め指定しておくものですが、コールバックパターンには処理の実行順序が分かりにくいという問題があります。
それに対し、kotlinのsuspendを利用すると、非同期処理の利点を活かしつつ同期処理のように上から順番に処理を実行することができます。

元々suspend関数として対応している機能であればよいのですが、利用したいライブラリがたまたまコールバックパターンだったとき、それをsusupend関数として直列的に扱えるようラップしてみます。
例えば、以下のようなコールバックを利用している処理があったとします。

fun someFunctionWithCallback(param: String, callback: (result: Boolean) -> Unit)
{
    ...
}

fun procCallabck()
{
    Log.d(TAG, "非同期処理 ログ1")

    someFunctionWithCallback("some param") { result: Boolean ->
        Log.d(TAG, "非同期処理 ログ2")
        if (result) {
            // do something when succeeded...
        } else {
            // do something when failed...
        }
        Log.d(TAG, "非同期処理 ログ3")
    }

    Log.d(TAG, "非同期処理 ログ4")
}

上記のprocCallabckの処理にて4箇所でログ出力していますが、そのログが表示される順序は以下の通りとなり、上から順番には処理されません。
– 非同期処理 ログ1
– 非同期処理 ログ4
– 非同期処理 ログ2
– 非同期処理 ログ3

そこで、このsomeFunctionWithCallbackをsuspend関数でラップし、上から順番に処理できるようにします。

suspend fun serialFunction(param: String): Boolean
{
    return withContext(Dispatchers.IO) {
        val future = CompletableFuture<Boolean>()

        someFunctionWithCallback(param) { result ->
            future.complete(result)
        }

        try {
            future.get(10L, TimeUnit.SECONDS) // withContextの戻り値
        } catch (ignore: TimeoutException) {
            false // withContextの戻り値
        }
    }
}

新たなsuspend関数としてserialFunctionを作成しました。
ポイントとなるのは、CompletableFutureクラスです。future.get(10L, TimeUnit.SECONDS) のところで非同期処理の完了を待ち、完了時に結果を受け取ることができます。
コールバック関数の中でfuture.complete(result)という処理がありますが、これが呼び出されることでCompletableFutureクラスの待ちが完了し、結果が返されます。

これを使って、上の処理と同様に4つのログを出力してみます。

CoroutineScope(Dispatchers.IO).launch {
    Log.d(TAG, "非同期処理 ログ1")

    val result = serialFunction("some param")

    Log.d(TAG, "非同期処理 ログ2")
    if (result) {
        // do something when succeeded...
    } else {
        // do something when failed...
    }
    Log.d(TAG, "非同期処理 ログ3")

    Log.d(TAG, "非同期処理 ログ4")
}

これを実行すると、以下の通りにログも上から順番に表示されるようになります。
– 非同期処理 ログ1
– 非同期処理 ログ2
– 非同期処理 ログ3
– 非同期処理 ログ4


上記の処理で、val result = serialFunction(“some param") のところで関数の完了を自動的に待ってくれるため、非同期処理の恩恵を受けつつも、上から順番に処理が行われます。
なお、suspend関数はCoroutineの中でしか実行できません。上記の処理は CoroutineScope(Dispatchers.IO).launch で囲まれていますが、このブロックが無いとエラーになります。

これで、同期処理のように読みやすいプログラムに書き換える事ができました。
また、コールバック処理のブロックがなくなったことで、インデントが揃って処理の見通しも良くなりました。

弊社ではAndroidアプリ、iOSアプリの開発も承っておりますのでお気軽にご相談ください。