Takuji->find;

Sansan株式会社でAndroidのテックリードをやってます、技術的な記事を書いているつもり

既存パラダイムとKotlin Coroutinesの共存

この記事は DroidKaigi 2020, day 1 17:00-17:40で発表される予定だった内容を書き起こしたものです。

droidkaigi.jp

他にも発表予定だった資料のAGENDAを1項目ごとに記事にして公開する予定です。

  • 既存パラダイムとの共存 (この記事)
  • ライブラリーと組み合わせる
  • Activity/Fragmentで利用する
  • ViewModelで利用する
  • LiveDataとの使い分け
  • 例外処理
  • キャンセル可能な作りにする
  • テスト

はじめに

Kotlin Coroutinesが正式リリースされて2年ちょっとが過ぎ、もはや私たちのAndroidアプリ開発には欠かせないものとなりました。

コルーチンはRxJavaのようなストリームやコールバック方式と比べて非同期的な処理を直感的に記述でき、これまでRxJavaを使っていたようなユースケースをある程度置き換えられるようになっています。 ​ ​ 私はこの2年と少しでKotlin Coroutinesを複数のアプリに導入し既存のパラダイムの置き換えを進めてきました。

Kotlin Coroutinesの導入を行ったことでAndroidアプリ開発が楽になりましたが、大変になったこともありました。

恐らくKotlin Coroutinesが難しく感じて導入をためらっている方もいるでしょう。 ​

このシリーズでは、既存のAndroidアプリにKotlin Coroutinesを導入したことによってAndroidアプリ開発がどう変わったか、

Kotlin Coroutinesの導入を行ったことで困ったことをどう解決したかなどを紹介します。

皆様のKotlin Coroutinesの導入の参考になれば幸いです。

また、Kotlin Coroutinesに関してまだ何も知らないという方はこのシリーズを読む前に以下の記事か、Kotlin Coroutinesの公式ドキュメントを一読されることをおすすめします。

blog.takuji31.jp

既存パラダイムとの共存

既存アプリにKotlin Coroutinesを導入するにあたっておそらく最初の壁となるのが既存のパラダイムへの影響でしょう。 Kotlin Coroutinesで書くと既存のRxJavaやイベントハンドラーのようなものをコルーチンに置き換えたくなるはずです。

既存のパラダイムを全部置き換えるのは工数がない、ライブラリーやフレームワークのコードは簡単には変えられない、といった問題で現実的ではなく実際は一部を置き換えるということになるでしょう。

  • 特定の機能だけKotlin Coroutinesで書く
  • 特定のレイヤー(ViewModel/Repository/Data層など)だけKotlin Coroutinesで書く

どちらの方法で進めても途中で必ず既存のパラダイムとKotlin Coroutinesの仕組みとの変換が必要になってきます。 この記事ではRxJavaやコールバック、イベントリスナーといったような仕組みとKotlin Coroutinesを繋ぎ込む方法を紹介します。

RxJavaとKotlin Coroutines

RxJavaはおそらく現代のAndroidアプリ開発Jetpackの次くらいに一番よく使われているライブラリーではないでしょうか。

リアクティブプログラミングによってAndroidアプリ開発者は複雑な非同期処理を行う際のコールバック地獄から解放されました。

一方でコールバック地獄の次はOperator地獄、実行するスレッドを切り替えるための仕組みであるSchedulerが分かりにくい、Operatorをめちゃくちゃ組み合わせたら書いた本人以外何やってるのか分からないコードが完成する、などと人類には早すぎた感も否めないように思えます。

RxJavaとKotlin Coroutinesは kotlinx-coroutines-rx2kotlinx-coroutines-reactive を使うことで相互運用が可能です。

RxJavaをコルーチン内で使う

RxJavaの各種ストリームをコルーチン内で使うための拡張関数が kotlinx-coroutines-rx2kotlinx-coroutines-reactive (Reactive Streamsの Publisher 実装である Flowable はこちら) には用意されています。

以下のようなRepositoryのコードがあるとします。

interface UserRepository {
  fun getCurrentUserId(): Maybe<UserId>
  fun getUser(userId: UserId): Single<User>
  fun userStatus(): Observable<UserStatus>
  fun userStatusFlowable(): Flowable<UserStatus>
}

このRepositoryのメソッドはそれぞれコルーチン内で以下のように使えます。

val repository : UserRepository

scope.launch {
  // get curent user id
  val userIdOrNull: UserId? = repository.getCurrentUserId().await()

  userIdOrNull?.let {
    // get user
    val user: User = repository.getUser(it).await()
  }

  // await first user status
  val userStatus: UserStatus = repository.userStatus().awaitFirst()

  // also ok
  val userStatus: UserStatus = repository.userStatusFlowable().awaitFirst()
}

もちろんそれぞれのストリームがsubscribeした時に同期的に処理をする作られている場合はsubscribeしたその場で処理が同期的に行われてしまうので、通信やディスクI/Oを伴う処理が含まれている場合は以下のような対策が必要です。

  • withContext {} を利用してメインスレッド上で実行されないようにする
  • RxJavaのSchedulerを使って処理を非同期に行う

また、これ以外にも ObservableFlowableFlow に変換する拡張関数も用意されています。 複数流れてくる値をコルーチンで処理したい場合にはこちらを使います。

interface UserRepository {
  fun userStatusFlowable(): Flowable<UserStatus>

  fun userStatusFlow(): Flow<UserStatus>
    = userStatusFlowable().asFlow()
}

コルーチンを使ってRxJavaのストリームを作る

RepositoryをKotlin Coroutinesを使って書くようにしたはいいがViewModelがまだRxJavaで〜とかUseCaseで他のRxJavaのストリームと組み合わせないといけなくて、みたいなことがあった場合にはSuspending functionやFlowを使ってRxJavaのストリームを作ることもできます。

interface UserRepository {
  suspend fun getCurrentUserId(): UserId?
  suspend fun getUser(userId: UserId): User
  fun userStatus(): Flow<UserStatus>
}

rxXXXX {} という関数が用意されているので、この関数のラムダ式内でSuspending functionやFlowを使うと簡単にRxJavaのストリームを作ることができます。

val repository: UserRepository
val currentUserId: Maybe<UserId>
  = rxMaybe { repository.getCurrentUserId() }

val userId: UserId
val user: Single<user>
  = rxSingle { repository.getUser(userId) }

val userStatusFlowable: Flowable<UserStatus>
  = rxFlowable { producer ->
    repository.userStatus().collect { producer.send(it) }
  }

また、 Flow を簡単に ObservableFlowable にも変換できます。

val userStatusFlowable: Flowable<UserStatus>
  = repository.userStatus().asFlowable()

val userStatusObservable: Observable<UserStatus>
  = repository.userStatus().asObservable()

その他

ここで紹介した以外にも kotlinx-coroutines-rx2 にはいくつかの関数が用意されてますので、詳しくは公式ドキュメントをチェックしてください。

kotlin.github.io

コールバックとKotlin Coroutines

ここでいうコールバックとは以下のようなものです。

  • 何かしらの非同期な処理を行って、成功時に結果をメソッドやラムダ式に返す
    • 値を返すのは1回だけ
  • 例外が起きた時も例外をメソッドやラムダ式に渡す
    • 成功時と同じメソッドでもいいし別のメソッドでもいい
  • 途中でキャンセル可能でもよい

コールバックをSuspending functionに変換するには以下の2つの関数が使えます。

  • suspendCoroutine
  • suspendCancelableCoroutine

2つの違いは名前通りキャンセル可能かどうかなので、必要に応じて選択してください。

これらの関数はKotlin Coroutinesの低レベルAPIContinuation を受け取ってこのインスタンスに値を渡すことで再開できる関数を作るといったものです。

例えば以下のようなHTTPクライアントがあるとします(実装は省きますが名前で雰囲気を掴んでいただく感じで)

interface HttpClient {
  fun myUser(callback: HttpClient.Callback<User>): Request

  interface Callback<T> {
    fun onSuccess(res: T)
    fun onFailure(e: HttpException)
  }

  interface Request {
    fun cancel()
  }
}

これをSuspending functionに変換するとこうなります。

suspend fun HttpClient.myUser() : User
  = suspendCoroutine { cont ->
    myUser(object: HttpClient.Callback<User> {
      fun onSuccess(res: User) {
        cont.resume(res)
      }

      fun onFailure(e: HttpException) {
        cont.resumeWithException(e)
      }
    })
  }

あとはコルーチン内で普通に他のSuspending functionと同じように使えます。

scope.launch {
  val httpClient: HttpClient

  val user: User = httpClient.myUser()
}

イベントリスナーとKotlin Coroutines

ここでいうイベントリスナーとは以下のようなものです。

  • 何らかのイベントが起きるとメソッドやラムダ式が呼ばれる
  • Listenerを登録すると延々と値が流れてくる
  • 不要になったら明示的に登録解除する必要があったりなかったり

複数の値が流れてくるのでFlowが使えます。

ただしFlowを作るための一番簡単な関数である flow {} は同じコルーチン内からしか値を送ることができません。こういったコルーチンのスコープ外から複数の値を送るFlowを作るのには callbackFlow {} を使います。

似たような関数に channelFlow {} がありますが、こちらはFlowが閉じられるのを待つ awaitClose {} を呼び出したかどうかのチェックがありません。

val View.onClick: Flow<View> = callbackFlow {
  setOnClickListener { view ->
    offer(view)
  }

  awaitClose {
    setOnClickListener(null)
  }
}

この関数で生成されたFlowをcollectすればクリックしたイベントがコルーチンに送られます。

scope.launch {
  val onClick = view.onClick

  onClick.collect {
    println("Collect")
  }
}

ただしこの方法だと Flow がコールドストリームであることに影響して、 collect {} を実行するまでリスナーはセットされませんのでご注意ください。

最後に

この記事では既存のパラダイムとKotlin Coroutinesの共存方法について紹介しました。

Kotlin Coroutinesを使いたいが、既存のパラダイムとの組み合わせ方が分からない!という方の助けになれば幸いです。

次回はAndroidアプリでよく使われる既存のライブラリーとKotlin Coroutinesを組み合わせる方法について紹介したいと思います。