もう迷わないCoroutines(記事版〜余談編〜)
はじめに
この記事はMoney Forward Engineering 1 Advent Calendar 2022 17日目の投稿です。
16日目はHidemasa ShimazuさんでDependabot 運用を自動化したいでした。
本日は掲題について書いていきたいと思います。
本稿について
先日のKotlin Fest 2022で「もう迷わないCoroutines 〜suspend funとChannelとFlow〜」というタイトルでお話をさせていただきました。
本記事はそのときの余談、記事版として位置づけ、補足・解説にフォーカスした内容になっています。
目次
- あらすじ
- 補足の話その1:Case.1:Callbackで値を受け取りたいとき/Case.2:Listenerで値を受け取りたいとき
- 補足の話その2:Case.3:通信処理の置き換え
- 特徴まとめ
- FAQ
- まとめ
あらすじ
セッションでは、Kotlin Coroutines(以降コルーチン)の基本を実用面に寄った範囲でおさらいし、suspend関数、Flow、Channelの特徴を整理しました。それらの特徴を踏まえて、実際にあり得るケースを想定し、コルーチンの各機能をどう使い分けるのがいいか?を解説・提案させていただいた、という内容になっています。
詳しくは動画も既に公開されておりますので、お時間のある際にご覧いただけますと幸いです。
補足の話その1:Case.1:Callbackで値を受け取りたいとき/Case.2:Listenerで値を受け取りたいとき
Case.1ではsuspendCancellableCoroutine
を用いた例、Case.2ではcallbackFlow
を用いた例をご紹介しましたが、それぞれ実現方法は他にも色々あると思っています。
例えばCase.1ではワンショットでの値取得でしたが、呼び出し元に対してFlow
で揃えたい、というケースも少なくないと思います。
fun getGyro(): Flow<Gyro> = callbackFlow {
val listener = object : Callback {
override fun onNextValue(value: Gyro) {
trySend(value)
cancel()
}
override fun onError(cause: Throwable) {
cancel(CancellationException("Error", cause))
}
}
sensorManager.addListener(listener)
sensorManager.start()
awaitClose {
sensorManager.stop()
sensorManager.removeListener()
}
}
このくらいの処理であれば、suspend関数ですでに実装されている処理をFlow
に置き換えたい、といった場合でもそこまでのコストではなさそうに思いますが、より変更コストを低く抑えたい意思が強ければsuspend関数に対してasFlow
を使用するという選択肢も検討できるかもしれません。
suspend fun getGyro(): Gyro
fun gyroFlow(): Flow<Gyro> = ::getGyro.asFlow()
launch {
gyroFlow.collect {
}
}
ちなみに、callbackFlow
の理解にはflowWithLifecycle
のソースコードが個人的には非常に参考になりましたので、ここに共有します。
@OptIn(ExperimentalCoroutinesApi::class)
public fun <T> Flow<T>.flowWithLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): Flow<T> = callbackFlow {
lifecycle.repeatOnLifecycle(minActiveState) {
this@flowWithLifecycle.collect {
send(it)
}
}
close()
}
補足の話その2:Case.3:通信処理の置き換え
Rxを使った通信処理を実装している環境におけるコルーチンへの置き換えを想定してお話をしました。
ここではkotlinx.coroutinesのreactive modulesについても触れました。以前はObservable
に対してopenSubscription
を使うことでChannel
として扱うこともできたのですが、現在はasFlow
を使ってFlow
に変換するというAPIに変わっており、openSubscription
は使えなくなっています。以前に記事を書いた時点でObsolete API
として扱われていたので、当時から利用を控えていたチームも少なくなかったかもしれないですね。
また、reactive modulesを使わない選択をした場合でも、引数(Request Bodyなど)が不要な通信リクエストであればasFlow
で簡単にFlow
にすることができますし、引数が必要な場合でもflow
ビルダーを用いて値を下流に流すのは簡単なので、エンドポイントひとつひとつに対しての変換だけで言うとそう難しくはなさそうかなと思います。
特徴まとめ
suspend関数、Flow、Channelそれぞれの特徴をまとめた表がこちらです。
表からも見て取れますが、継続的に値を購読したいケースにおいては、新規に実装する場合は基本的にはFlow
を採用するで問題ないかなと思っています。
これにはいくつか理由がありますが、Flow
に比べてChannel
は実装・設計として複雑といった話ですとか、Channel
は異なるプロセスのコルーチンにデータを送る必要があるといったケースなどで使用されますが、同時に存在するコルーチン間でデータを扱うためには同期をとる必要がありどうしてもコストが掛かる、といった点が挙げられます。
セッションではあくまで各機能での比較としているため、例えば、こういった場合はSharedFlow
で作成しておいてtake
オペレータを挟んでFlow
として公開するのが良いよね、ワンショットな取得処理だけどFlow
を使ってStreamとして扱いたいな、といった具体的な実際のユースケースまでは踏み込んでいませんでした。
より実践的で具体的なケースのお話をするのは話の構成として拡がりすぎてしまいそうと考え、セッションでは敢えて削っています。今後もし機会があれば、そういった具体的なケースの紹介・提案に絞ったお話をするのも面白そうかなと思っています。
private val mutableStateFlow = MutableStateFlow<State?>(null)
val stateFlow = mutableStateFlow.asStateFlow().filterNotNull()
また、フローチャートに整理されているとより分かりやすいかなとも思っていますので、こちらは作成したら本記事に追記する形で公開しようかと思っています。
FAQ
セッション内でいただいた質問について、加筆修正しつつで改めて回答を掲載したいと思います。
callbackFlow
だとキャンセルできない?
Q1. できます。callbackFlow
のsuspendブロック内ではProducerScope
にアクセスすることができるため、cancel
もisActive
も呼び出すことができます。
Q2. ワンショットでリスナーから値を取得してcloseするという使い方はあまり良くない?
問題ないと考えています。呼び出し元に対してFlow
で揃えたい、という判断もあるかと思っていますし、そういった場合はcallbackFlow
を使ったほうが実装に無駄がないケースもあると思っています。
Q3. Rx+Retrofitの置き換えのベストプラクティスを知りたい
同じく私もとても興味があります。セッションでご紹介した例ではRx→Flow/Channelの変換でボトルネックになるケースがあったことを判断材料に、リスク回避としてreactive modulesの利用には頼らないという実装を挙げました。
もちろん既存の状態に依る話ではありますが、どういった戦略で臨むのが良いか?という話は色んな事例を聴いてみたいところがあります。
Q4. blockingが必要な場面、敢えてコルーチンを使わないというケースはあるか?
SharedPreferences
にアクセスする際に泣く泣くblockingしたことがあります。
SharedPreferences
は色んなところで呼び出されがちというのと、コルーチン以外の非同期・スレッド処理が既に多く使われていたりすると、行儀よい形に修正するために掛かるコストや影響範囲などを考慮すると止むなく...
DataStoreへの置き換えでも同様の問題がでてくると思っていますので、戦略を練っていきたいなというお気持ちです。
Q5. Cold Flowではメモリ使用量とかは気にならない?
現状あまり気になったケースはなかったかなと思います。
とはいえCold Flowはcollect
のたびにそれぞれで起動されることにはなるので、ビルダー関数の中でインスタンス化するものが過度に増殖しないようにする、というのはおさえておきたいところと思ってます。
Q6. 現時点でRxからsuspendに置き換わっている状態でさらにFlowに置き換える意味はあるか?
意味はあると思います。Flow
自体はsuspendじゃない世界でも扱えるため、suspend関数を呼び出すよりも取り回しがしやすいという側面はあるかなと思います。また、Flow
にしてStreamで扱えたほうが勝手がよい、という判断もあるかと思っているので、置き換えることでのメリットも考えられるかなと思っています。
まとめ
Kotlin Fest 2022登壇時には話しきれなかった話のご紹介などをさせていただきました。
FAQに関してはまだ拾い切れていないものや気になることなどあればご教示いただけますと幸いです。
また、コルーチン以外にもKotlinには便利な機能が多くあります。
そんなKotlinの次バージョン、1.8.0のRCが先日リリースされ、その足音が近づいてきています。
引き続きキャッチアップしていき、コルーチンを、そしてKotlinを愛でていきたいなと思っています。
皆さんも是非これからもKotlinを愛でていきましょう。
Discussion