読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

Kotlin 1.1 async/awaitの仕組みと使い方の概要 for Android

Kotlin Advent Calendar Android

これはKotlin Advent Calendar2016の19日目のエントリです。

本エントリではKotlinの次期バージョン(1.1)で導入されるコルーチンと、その実装のひとつであるasync/awaitについて解説します。

今回書いているコードはGitHub - sys1yagi/kotlin-async-await-sample: yey!に置いています。

Kotlin 1.1の様子

Kotin 1.1は2016年7月にFirst glimpse of Kotlin 1.1: Coroutines, Type aliases and moreで変更の概要とEAPが公開されました。コルーチンのほかにタイプエイリアスやメソッド参照、ラムダ式での引数の分解宣言などなど様々な便利な機能の追加が予定されています。2016年12月の時点で1.1-M03が公開されています。

Kotlin 1.1の仕様はKEEPで検討が進められていて、最近では1.1以降のものについても検討が走っているようです。Github ProjectsのKEEP Kotlin 1.1 では1.1に絞ったissueの進捗を確認できます。

コルーチンとはなにか

コルーチンは任意の箇所で一時停止ができる計算と考えることができます。一般的な関数は処理の開始からリターンまででひとつの処理を表しますが、コルーチンは処理の途中に中断するポイントを明示できます。中断するごとに値を返せるほか、中断したときの状態を保ったまま任意のタイミングで再開ができるので、非同期処理や遅延リストの作成(ジェネレータ)を簡潔に書けるようになります。

次のコードはasync/awaitを用いた非同期処理の待ち合わせの例です。

asyncAndroid {
  val user = userApiClient.get(id).await() // ここで中断して非同期処理
  val articles = articleApiClient.getArticles(user).await() // ここで中断して非同期処理
  // 取得完了
  showArticles(articles)
}

一般的な非同期処理はコールバックを用いてネストが深くなりますが、コルーチンを用いるとフラットに記述できるようになります。

Kotlin 1.1のコルーチンのアプローチ

Kotlin 1.1ではコルーチンの処理全体を「状態」の集まりとしてステートマシンに変換することで、JVM上での動作を実現しています。

コルーチンのコードをステートマシンとして解釈するために、コルーチンの処理を表すcoroutineキーワードと、中断点を示すためのsuspendキーワードが追加されています。これらのキーワードによって「コルーチンの実装」を言語機能としてサポートします。

Kotlin 1.1で提供されるasync/awaitやジェネレータは、コルーチンの実装としてライブラリの形式で提供されます。

Kotlin 1.1におけるコルーチンの実現方法の詳細についてはTechBoosterがC91で頒布する「なないろAndroid」に詳細な解説を書いていますのでぜひ読んでみてください。

Kotlin 1.1を導入する

Kotlin 1.1はまだEAPの段階なので導入のためにいくつか設定が必要です。次のようにEAPmaven urlを追加した上で1.1-M03などのバージョンを指定してください。差分のみ記載しているので適宜必要なものを設定してください。

// build.gradle
buildscript {
  ext.kotlin_version = '1.1-M03'
  repositories {
    maven { url = 'https://dl.bintray.com/kotlin/kotlin-eap-1.1' }
  }
  dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}
repositories {
  maven { url = "https://dl.bintray.com/kotlin/kotlin-eap-1.1" }
}

// module/build.gradle
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

dependencies {
  compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

Koltin PluginをEAP 1.1にする

Android Studio向けのKotlin PluginもEAP版が提供されています。[Tools]-[Kotlin]-[Configure Kotlin Plugin Updates]でUpdate channelを'Early Access Preview 1.1'に変更し、プラグインをアップデートしてください。

async/awaitライブラリを導入する

Kotlin 1.1向けのコルーチン実装はkotlinx.coroutinesで開発されています。現時点では次の3つがbintrayで配布されています。

  • kotlinx-coroutines-generate : ジェネレータ
  • kotlinx-coroutines-async : async/await
  • kotlinx-coroutines-rx : RxJavaを用いたasync/await

kotlinx-coroutines-asyncはasync/awaitのインタフェースとしてCompletableFutureを用いています。CompletableFutureはJava 8のクラスなので、Androidで利用しようとするとminSdkVersion 24が必要となります。そのためAndroidでasync/awaitを使う場合はkotlinx-coroutines-rxを選択することになります。

kotlinx-coroutines-rxを導入する設定は次の通りです。スレッド操作のためにrxbinding-kotlinも入れておきます。

buildscript {
  ext.kotlin_coroutines_version = '0.1-alpha-2'
}
repositories {
  maven { url = "https://dl.bintray.com/kotlin/kotlinx.coroutines" }
}
dependencies {
  compile "org.jetbrains.kotlinx:kotlinx-coroutines-rx:$kotlin_coroutines_version"
  compile('com.jakewharton.rxbinding:rxbinding-kotlin:1.0.0') {
    exclude group: 'io.reactivex', module: 'rxjava'
    exclude group: 'com.android.support', 'module': 'support-annotations'
    exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
  }
}

Androidでasync/awaitを使う

kotlinx-coroutines-rxはasyncRx関数を提供します。asyncRx関数は引数のブロック内で呼び出した中断関数を直列に連結し、最後の処理の値を戻り値としてObservableで包みます。

val observable = asyncRx<List<Article>> {
    val user = userApiClient.get(1L).awaitSingle() // ここで中断と再開をする
    articleApiClient.getArticles(user).awaitSingle() // ここで中断し、戻り値を返す
}
observable.subscribe { articles ->
    // do something.
  }

上記の例ではawaitSingle()が中断関数です。この関数はObservableの拡張関数として定義されています。このほかにawaitFirstawaitLastがあります。中断関数の種類や名前はコルーチンの実装に依存します。

asyncRx関数の機能は、Observableの中断関数の呼び出しにもとづいて直列に処理を連結することのみで、実行スレッドの管理等は行ってくれません。非同期処理をするために次のように書く必要があります。

val observable = asyncRx<List<Article>> {
  val user = userApiClient.get(1L)
    .subscribeOn(Schedulers.io())
    .awaitSingle()
  articleApiClient.getArticles(user).awaitSingle()
}
observable
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe { articles ->
      // do something.
  }

かなり不格好ですね。このまま実用するのはちょっと冗長に思えます。というか普通にRxJavaを使えばいいような気がしますね。

asyncAndroidを実装する

asyncRxをそのまま使ってもあまり便利ではないので、次の要件を満たす独自のコルーチンとしてasyncAndroid関数を実装してみましょう。

  • Observableは常にsubscribeOn(Schedulers.io())observeOn(AndroidSchedulers.mainThread())で実行する
  • コルーチンの中で非同期、同期処理のすべてを記述できる

実装内容が次です。

package kotlinx.coroutines.android

import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription

fun asyncAndroid(
  coroutine c: RxAndroidController.() -> Continuation<Unit>
): CompositeSubscription {
  val subscriptions = CompositeSubscription()
  val controller = RxAndroidController(subscriptions)
  c(controller).resume(Unit)
  return subscriptions
}

class RxAndroidController internal
                        constructor(val subscriptions: CompositeSubscription) {
  suspend fun <T> Observable<T>.awaitSingle(x: Continuation<T>) {
    this.single()
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())
      .subscribeWithContinuation(x)
  }

  private fun <T> Observable<T>.subscribeWithContinuation(x: Continuation<T>) {
    val subscription = subscribe(x::resume, x::resumeWithException)
    subscriptions.add(subscription)
  }
}

このコードの説明は省略します。内容を読み解くにはTechBoosterがC91で頒布する「なないろAndroid」をぜひ参照してください。あるいはkotlin-coroutines-informal.mdを読むのもいいかもしれません。

asyncAndroid関数を使うと次のような記述ができるようになります。便利ですね。

asyncAndroid {
  val user = userApiClient.get(1L).awaitSingle() // 中断して非同期実行
  // UIスレッドで再開
  binding.textArea.text = "asyncAndroid loading articles..." 
  val articles = articleApiClient.getArticles(user).awaitSingle() // 中断して非同期実行
  // UIスレッドで再開
  binding.textArea.text = "asyncAndroid finish loading. size=${articles.size}"
}

おわりに

筆者はすでに個人のプロダクトでEAPのKotlin 1.1を導入しています。特にasync/awaitとタイプエイリアスとメソッド参照が便利です。正式版が楽しみですね。Kotlinサイコ-なのでぜひ皆さんも使ってください。

あわせて読みたい:コミックマーケット91 新刊情報と予約開始 | TechBooster