visible true

技術的なメモを書く

もうAndroidの非同期処理はasync/awaitでいいんじゃないかなぁと思った

Rx Ja Night Vol.2 - connpassで「 Androidの非同期処理をKotlinコルーチンで行う」という話をしてきました。

スライドで使っているコードは次のリポジトリに置いています。

github.com

今回取り扱った非同期処理の範囲

スライドやリポジトリのREADME.mdに大体書いているのですがコチラにも載せときます。 詳細な説明はスライドやリポジトリを参照してください。 次の非同期処理をコルーチンで実現します。

単発の実行
直列の実行
並列の実行
+
エラーハンドリング
キャンセル

環境

すべてKotlinが提供する標準の機能を用います。

implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.2-4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.16"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.16"

単発の実行

コルーチン(async/await)ではこういう風に書けます。

val job = launch(UI) {
    try {
        val shop = async(CommonPool) { shopApi.getShop(10) }.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

launch(UI)async(CommonPool)はコルーチンのビルダー関数と呼び、続いて渡すブロックをコルーチンにしてくれます。 ここではlaunch(UI)はコルーチン内をUIスレッドで実行する、async(CommonPool)はコルーチン内をスレッドプールで実行する、という理解でいいと思います。 毎度書くと冗長なので、次のようなパッケージレベル関数を用意して簡潔に書けるようにしたりできます。

fun <T> async(context: CoroutineContext = CommonPool, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T)
        = kotlinx.coroutines.experimental.async(context, start, block)

fun ui(start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit)
        = launch(UI, start, block)

スッキリ

val job = ui {
    try {
        val shop = async { shopApi.getShop(10) }.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

エラーについてもtry-catch式なので、様々な例外をよしなに扱えます。 uiの中でawait()して待ち合わせていて大丈夫かって気になりますが、 Kotlinコンパイラがよしなにコルーチン内のコードを状態毎に分解していて、 await()の呼び出しの所で処理を中断しているので大丈夫です。 await()が完了すると結果がdispatchされ、Handler.post()で続きを実行するといった具合です。

直列の実行

await()を後続のasyncの前か、その中で呼び出せばOK。

val job = ui {
    try {
        val userJob = async { userApi.me() }
        val subscriptionShopsJob = async { subscriptionShopApi.getSubscriptionShops(userJob.await().id) }
        val subscriptionShops = subscriptionShopsJob.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

並列の実行

並列で実行したいasyncを呼び出したあとにawaitすればよい。

val job = ui {
    try {
        val userJob = async { userApi.me() } // start immediately
        val shopJob = async { shopApi.getShop(10L) } // start immediately
        val user = userJob.await()
        val shop = shopJob.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

キャンセル

launch(UI)から返るJobcancel()を呼び出せばよい。

job.cancel()

するとコルーチンの中でCancellationExceptionが飛ぶ。

job = ui {
    try {
        val shop = async { shopApi.getShop(10) }.await()
        // success
    } catch(e: CancellationException) {
        // cancel
    } catch (e: Exception) {
        // error
    }
}

感想

特にデメリットもなさそうだし圧倒的に簡潔だし、もう非同期処理はasync/awaitでいいやって思った。 なんてったって非同期処理のための実装だからね。 このほかproducerやactorなどを使うと更にUIイベントのストリームとかイベントバスみたいなこともできそうだけど、 この辺はそのために用意されているかでいうと微妙な気もするのでどちらでもいい気もしている。 一応experimentalなんだけど、裏側の実装が変わる事はあっても利用コード側はそこまで影響受けないだろうしいいんじゃないかなとか。 プロダクトでも入れ始めているしそうしようみたいな気持ち。

参考

これ読んだらもう使ってよしだと思う。