いかがでしたか?(先手必勝)
先日ふと、普段使っているkotlinx.coroutines、どういうところに利点があるんだっけ? と思い、自分が知っていることを軽くまとめようとしたところ軽くない量になったので、まとめたものを自ら公開処刑にします。
そもそもsuspend funって何?
suspendポイントで一度ライブラリ側(kotlinx.coroutinesとかComposeとか)に制御を戻し、然るべきタイミングで再度suspendポイントから続きの処理を再開できる関数。
例
val seq = iterator {
yield(1)
yield(1)
yield(2)
yield(3)
}
seq.take(3).joinToString() // "1, 1, 2"
このIteratorはnextが呼ばれるたびに次のyieldまでを実行して、yieldされた値をnextから返すような動作をする。
最初に[1, 1, 2, 3]を生成しているわけでもないしマルチスレッドで動作しているわけでもない。だからtake(3)すれば3番目のyieldまでしか実行されない。無限ループ内でyieldしようが必要以上にはyieldされない。
例
launch {
repository.getHogehoge()
}
親の顔より見たコード。ネットワーク処理だけを別のスレッドで行う。
ネットワーク処理の完了を待つ間、メインスレッドは一度この関数の仕事から解放されていて、通常通り画面の描画やタッチイベントの処理等の仕事に戻っている。
ネットワーク処理の完了後、メインスレッド上で続きの処理を行う。
技術的には?
suspend fun hogehoge() {
println("1")
val i = suspendFunction1()
println("2")
suspendFunction2()
}
こういうsuspend funがあったとすると次のような関数とクラスが生成される
class HogehogeContinuation : Continuation {
var label = 0
var result: Result<Any?>
override fun resumeWith(result: Result<Any?>): Any? {
this.result = result
return hogehoge(this)
}
}
fun hogehoge(continuation: HogehogeContinuation): Any? {
when (continuation.label) {
0 -> {
println("1")
continuation.label = 1
val s = suspendFunction1(continuation)
if (s === COROUTINE_SUSPENDED) { return COROUTINE_SUSPENDED }
val i = s.getOrThrow()
// 本来のKotlinの文法ではここでwhenから抜けるが、
// ここではJavaのswitch文のようにそのまま次の 1 -> のブロックに進む動作をイメージしてほしい
}
1 -> {
val i = continuation.result.getOrThrow() as Int
println("2")
continuation.label = 2
suspendFunction2(continuation)
if (s === COROUTINE_SUSPENDED) { return COROUTINE_SUSPENDED }
// whenから抜けずそのまま 2 -> のブロックに行きます
}
2 -> {
return Unit
}
}
}
ライブラリ側(kotlinx.coroutinesとか)はContinuation.resumeWith(Result<Any?>)
を呼び出せば続きの処理を実行できるというワケ
厳密に言うと完全にこうではないので、完全なものを知りたい方はより詳しい資料を参考にしてほしい
kotlinx.coroutinesって何?
Kotlin自体の開発元であるJetBrainsが開発してるライブラリのひとつ。なのでKotlinの標準ライブラリには含まれていないもののデファクトスタンダードになっている。(ほかのkotlinxもだいたいそう)
ちなみにsuspend fun自体は言語仕様なのでsuspend funに関係するクラス等は標準ライブラリのkotlin.coroutinesパッケージ(xがつかない方)にある。Continuationとか
launchとasync
kotlinx.coroutinesでコルーチンを起動する方法としてlaunchとasyncがあるけど、この2つはどう違うの?
早い話、コルーチンからなんらかの結果を受け取る場合asyncを使うと思ってよい。
val deferred = async {
delay(7_500_000L * 365 * 24 * 60 * 60 * 1000)
42
}
assert(deferred.await() == 42)
そうではない、結果がUnitになるコルーチンならlaunchで起動しよう。
launchの返り値の型はJob
、asyncの返り値の型はDeferred
。DeferredはJobのサブタイプになっている。
いわゆるFutureやPromiseと呼ばれるパターンなのだが、KotlinはJavaやJSのライブラリを利用できる言語であるため、JavaのFuture、JSのPromiseとの衝突を避けてDeferredと命名したらしい。
他言語ではasync/awaitを言語仕様としていることも多いが、Kotlinはasync/awaitはsuspend funを使って実装したライブラリのひとつという位置づけ。べつにasyncを使わなくても非同期処理はできるので他言語から来た人はkotlinx.coroutinesのasync/awaitという命名に騙されて同じ感覚でawaitを使ってしまわぬよう気をつけてほしい。
というわけなのでぶっちゃけコルーチンを起動するときはほとんどlaunchを使う。
ちなみにasynchronousの発音がeɪsíŋkrənəsであるためasync/awaitはエイシンク/アウェイトと読むのが正しそうである
CoroutineDispatcher
kotlinx.coroutinesにおいてsuspendしていたコルーチンが再開するときの挙動を決めるもの。
Dispatchers.Mainを使うとsuspendポイントから再開するとき、続きの処理が必ずメインスレッドで実行される。
簡単に変えられます
launch(Dispatchers.Main) {
// ここはメインスレッド
withContext(Dispatchers.IO) {
// ここはIO処理用のバックグラウンドスレッド
}
// ここはメインスレッド
}
CoroutineContext
その名の通り、実行中のコルーチンの状態。
扱い方はMapに近い。coroutineContext[CoroutineDispatcher]
でコルーチンにどのDispatcherが指定されるのか知ることができたりする。
実装は不変で、値の追加、変更、削除を行うたびに新しいCoroutineContextが生成される。
値の追加はMapの記法ではなく+
を使う。
× coroutineContext[CoroutineDispatcher] = Dispatchers.Main
○ coroutineContext + Dispatchers.Main
独特。
また、CoroutineDispatcher自体もCoroutineContextであるため、CoroutineContextが必要なところにDispatchers.Mainだけを渡すことも可能。
withContext(Dispatchers.Main) {}
withContext(Dispatchers.Main + CoroutineName("coroutine-1")) {}
withContext(coroutineContext + Dispatchers.Main + CoroutineName("coroutine-2")) {}
つまり、より正しく言うと、+を使うことで左オペランドのCoroutineContextと右オペランドのCoroutineContextの両方を持つCoroutineContextが生成される。同じキーのCoroutineContextを+した場合、右オペランド優先。
結構難しそうに見えるけど、実際CoroutineContextのコードを見てみると思った以上にシンプルに実装されてておもしろい。
Continuation内に保持されるので標準ライブラリにあるけど、標準ライブラリで使われているところを見たことがない
キャンセル
kotlinx.coroutinesにはキャンセルという概念がある。
kotlinx.coroutinesが提供するsuspend funはすべてキャンセル可能で、suspendしている間にキャンセルされた場合はCancellationException
をスローして続きの処理を行わせない。
launch {
val job = launch {
delay(3000L) // ここでCancellationExceptionがスローされる
println("ここは実行されない")
}
launch {
delay(1000L)
job.cancel()
println("キャンセルしました")
}
}
あくまで「CancellationExceptionをスローしている」という仕組みであるところには注意。
delay
等でsuspendしている最中にキャンセルされた場合には即座にCancellationExceptionをスローできるが、そうでない場合は即座に実行が中断されるわけではなく、次にdelay
等のキャンセル可能なsuspend関数を呼び出すまでCancellationExceptionはスローされない。
Javaスレッドにはstop
という即座に処理を止めるメソッドがあるが、非推奨となった。
ファイルへの書き込み中など、処理が中断されてはいけないタイミングがあるため、いつでも即座に処理を中断できるのはよくないのだ。
キャンセルを考慮すべきシチュエーション
kotlinx.coroutinesで起動したコルーチンがCancellationExceptionをスローしてもアプリケーションはクラッシュしないので、キャンセルを過度に恐れる必要はない。
しかし一部のシチュエーションではキャンセルの可能性を考慮したほうがよいこともある。
中断してはいけない処理
先述のとおり、ファイルへの書き込みや、一時的にデータが不正な状態になる処理など、途中で中断してはいけないタイミングがある。
そういう類の処理中、suspend funを呼び出す際には要注意。
もっとも、そういうシチュエーションではキャンセル以外の失敗も想定してcatch, finally等を使う可能性が高いので、CancellationExceptionも一緒くたに処理できるのだが。
キャンセル以外の例外の処理
CancellationExceptionと他の例外を一緒くたにしてはいけないシチュエーションがあることも頭の片隅に置いておいたほうがよかったりする。
try {
somethingSuspend()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().recordException(e)
}
うっかりCancellationExceptionのことを忘れていた場合・・・
- キャンセルを誤って例外として扱ってしまい、なにもエラーは起きていないのにエラー処理が走る可能性がある
- CancellationExceptionを誤ってcatchした結果、実行してはいけない続きの処理を実行してしまう可能性がある
NonCancellable
キャンセルされること自体が不都合な場合はNonCancellable
を利用するとよい。
launch {
val job = launch {
withContext(NonCancellable) {
delay(3000L) // CancellationExceptionがスローされなくなる
println("ここが実行されるようになる")
}
}
launch {
delay(1000L)
job.cancel()
}
}
とはいえ、可能な限り早く処理を安全に中断できるのは単純にリソースを抑えるのにいい効果があるので基本的にはキャンセルは許容したい。特に、後述のAndroidライフサイクルによるキャンセルにおいては大きな恩恵を受けることができる。
CoroutineScope
launch
, async
など、kotlinx.coroutinesでコルーチンを起動する際に必要になるモノ。
多くの場合、lifecycleScopeやviewModelScopeなどAndroidフレームワークに対応したスコープが用意されているのでそれを使う。
lifecycleScope.launch {
}
ここで重要なのはlaunchに渡しているラムダ式(コルーチン本文)の型。
fun CoroutineScope.launch(block: suspend CoroutineScope.() -> Unit): Job
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
CoroutineScopeをレシーバーに持つ拡張関数になっている。
そのため、このラムダ式内ではthis is CoroutineScope
であり、メソッド呼び出しの際のthis
は省略可能なため、launchの中ではCoroutineScopeを明示することなくlaunchすることができる。(これ自体はKotlinerならよく見るテクニック)
lifecycleScope.launch {
assert(this is CoroutineScope)
launch {}
}
これにより、コルーチン内でコルーチンを起動した場合に親子関係ができる。
ではコルーチンの親子はお互いにどういう作用をもたらすのか?
1. CorutineContextの伝播
launchされた子コルーチンは親コルーチンのCoroutineContext
を引き継ぐ。
たとえばlifecycleScopeで起動したコルーチンはCoroutineDispatcherとしてMainを引き継いでいるし、さらに子コルーチンをlaunchすれば子コルーチンのCoroutineDispatcherにもMainが引き継がれる。
lifecycleScopeを使ってlaunchしたコルーチンがメインスレッドで実行されるのはこの仕様によるもの。
2. 子コルーチンの待機
親コルーチンはすべての子コルーチンが終了するのを待機する。
lifecycleScope.launch {
coroutineScope {
launch {} // 1
launch {} // 2
}
// 1, 2が完了しないとここに到達しない
}
3. キャンセルの伝播
たぶんこれがkotlinx.coroutinesで一番クセが強い。
親コルーチンがキャンセルされると子コルーチンがキャンセルされる。
lifecycleScope.launch {
launch {
delay(100L)
println("ここは実行されない")
}
cancel()
}
AndroidにおいてはlifecycleScopeがActivity, Fragmentのライフサイクルと紐付いており、Activity, Fragmentが破棄されるときにlifecycleScopeがキャンセルされる。結果としてActivityが破棄されるとlifecycleScopeを使って起動したコルーチンがすべてキャンセルされる。
これによってActivityが破棄されたあとも無駄に処理が走り続けるのを抑えられる。Fragmentが破棄されたあとにrequireActivity()
を呼び出してクラッシュなんていう不具合が減る。
4. 例外の伝播
例外がスローされたときにも子コルーチンがキャンセルされる。キャンセルの場合と違うのは、(CancellationException以外の)例外がスローされると例外が親コルーチンに伝播するところ。結果として兄弟、子孫、祖先などあらゆるコルーチンがキャンセルされる。
子コルーチンが例外をスローした時点で即座に親コルーチンに伝播しているところには注意。
それの何が問題かというと、例外を想定したasyncであってもawaitする前に親コルーチンが例外をスローするところ。例外を想定していないasyncの場合に最速でキャンセルさせるための仕様かと思われる。
lifecycleScope.launch {
val deferred = async {
throw Exception()
}
anotherSuspendFun() // ここで先に例外がスローされる
try {
deferred.await()
} catch (e: CancelletionException) {
throw e
} catch (e: Exception) {
println("catchできない")
}
}
この動作を抑え、親コルーチンに例外が伝播しないようにするためにはsupervisorScope
を使う。
lifecycleScope.launch {
+ val deferred = supervisorScope {
async {
throw Exception()
}
+ }
anotherSuspendFun() // 例外がスローされなくなる
try {
deferred.await()
} catch (e: CancelletionException) {
throw e
} catch (e: Exception) {
println("catchしました")
}
}
Channel, Flow
割愛。ここまで理解してればChannelはそんなに難しくない。
FlowはRxJavaのsuspend funで動作できる版みたいなもの。RxJavaが抱えていた多くの問題点も解消されてるので、いやRxJavaはもうええわと思わず選択肢に入れてほしい。ときにはストリームで処理したほうがスマートに済む場合があることを僕たちは知っているはずだ。
で、結局なにがうれしいのか?
多くの知識を要求してくるkotlinx.coroutinesというライブラリ、導入するとどういう利点があるのか?
kotlinx.coroutines特有の利点ではないもの
まずはkotlinx.coroutines特有の利点ではないもの。
つまり、たしかにkotlinx.coroutinesの利点ではあるが、kotlinx.coroutinesじゃなくてもできるもの。
私もたまにkotlinx.coroutinesでしかできないと思い込んでいることがあるので、自戒の念も込めて
軽量スレッドである
スレッドを大量に走らせると重すぎて落ちてしまうが、コルーチンをいくらlaunchしても落ちないという話がある。
// 落ちる
repeat(10000) { i ->
thread {
println(i)
}
}
// 落ちない
repeat(10000) { i ->
coroutineScope.launch {
println(i)
}
}
これは、kotlinx.coroutinesがタスクの数だけスレッドを用意するのではなく、予め用意したスレッドにタスクをうまく割り振るWorkerThreadの考えを利用しているためである。
であるから、WorkerThreadを実装しているjava.util.concurrent.Executorを使えばべつにコルーチンじゃなくても落ちない。
val executor = Executors.newFixedThreadPool(5)
repeat(10000) { i ->
executor.execute {
println(i)
}
}
asyncによる並列処理
asyncを複数使うことで、複数の処理を複数のスレッドで同時に処理させ、すべて終わるまで待つことが非常に簡単にできる。
coroutineScope.launch {
val deferred1 = async { hogehoge() }
val deferred2 = async { piyopiyo() }
deferred1.await()
deferred2.await()
}
が、これはExecutorでもできる。
executor.execute {
val future1 = executor.submit(Callable { hogehoge() })
val future2 = executor.submit(Callable { piyopiyo() })
future1.get()
future2.get()
}
処理のキャンセル
Job.cancel()やその他の理由によってコルーチンは処理の途中の安全なところでキャンセルすることができる。
coroutineScope.launch {
val deferred = async { hogehoge() }
deferred.cancel()
}
suspend fun hogehoge(): Int {
delay(7_500_000L * 365 * 24 * 60 * 60 * 1000) // ここでCancellationExceptionがスローされる
return 42
}
Executorでもできる。
executor.execute {
val future = executor.submit(Callable { hogehoge() })
future.cancel(true)
}
fun hogehoge(): Int {
Thread.sleep(7_500_000L * 365 * 24 * 60 * 60 * 1000) // ここでInterruptedExceptionがスローされる
return 42
}
キャンセルにあたるものはJavaのスレッドでは割り込みという。概念も仕組みもほぼkotlinx.coroutinesのキャンセルと同じもの。
対応関係を表にするとこんな感じになる。
コルーチン | スレッド |
---|---|
suspend | ブロック |
CoroutineScope | - |
CoroutineDispatcher | Executor |
launch, async | execute, submit |
Job, Deferred | Future |
キャンセル | 割り込み |
kotlinx.coroutines特有の利点
ではkotlinx.coroutinesじゃないとできないことってなんなの?
まあ、類似のライブラリを作ろうと思えば作れるので、本当にkotlinx.coroutinesでしかできないことはないのだが、少なくとも今までに登場してきた類似のライブラリではできなかったものたち。またJavaではsuspend funを言語仕様に持たない限り今後もできないものたち。
スレッドがブロックしない
通常、なんらかの処理の完了を待機するとき、スレッドはブロックする。
"なんらか"というのは例えば他のスレッドで行っている処理の待機、ユーザーからの操作の待機、特定の時間になるまでの待機などなど。
こういったあらゆる種類の「待機」において、スレッドはブロックする。この間スレッドは何もしない。何もできない。
しかしsuspend funは、この記事の最初に書いたとおり、suspendするときに一旦kotlinx.coroutinesに制御を戻す。後に然るべきタイミングでkotlinx.coroutinesはsuspendしていたところから続きの処理を再開できる。
コルーチン内でsuspend funを呼び、処理の完了を待つとき、スレッドはブロックせず一旦そのコルーチンの仕事から解放されている。
ブロックしないということは他のsuspend中でないコルーチンの実行を進められる。メインスレッドの場合はメインスレッドでしかできない画面の描画等の仕事をできる。
executor.execute {
Thread.sleep(10L) // ブロックする。
}
corounineScope.launch {
delay(10L) // ブロックしない。この待機中の10ミリ秒の間、スレッドは別の仕事をできる
}
executor.execute {
val future = executor.submit(Callable { hogehoge() })
future.get() // ブロックする。
}
coroutineScope.launch {
val deferred = async { hogehoge() }
deferred.await() // ブロックしない。この待機中の間、スレッドは別の仕事をできる
}
キャンセルの伝播
先述の通り、コルーチン内で子コルーチンを起動すると、親子間でキャンセルが伝播する。
使われることのない結果を計算し続けることを回避できる。
すでに不要になっているデータを取得することを回避できる。
Activityが破棄されたあとにContextへアクセスしてクラッシュすることを回避できる。
自力でスレッドの管理をしなくてよい
多くのフレームワーク(Android含め)はメインスレッドがブロックするとアプリケーションが応答なし(ANR)になる。
だから時間のかかる処理はメインスレッド以外のスレッドで行い、完了したらメインスレッドで続きの処理を実行する必要がある。
Executor等を使えばマルチスレッドで捌くことは意外と簡単にできる。
executor.execute {
val hogehoge = getHogehoge() // 時間がかかる
handler.post {
view.updateHogehoge(hogehoge)
}
val piyopiyo = getPiyopiyo() // 時間がかかる
handler.post {
view.updatePiyopiyo(piyopiyo)
}
}
が、そういう仕事はライブラリ等に押し付けて、ViewやViewModelの層のコードにスレッドに関係するコードを書きたくないというのがまずある。
kotlinx.coroutinesが導入されている環境では、suspend fun内部でスレッドを切り替え、処理が終わり次第再びメインスレッドで再開することができる。
lifecycleScope.launch {
val hogehoge = getHogehoge() // 時間がかかる(別のスレッドで実行される)
view.updateHogehoge(hogehoge)
val piyopiyo = getPiyopiyo() // 時間がかかる(別のスレッドで実行される)
view.updatePiyopiyo(piyopiyo)
}
繰り返しになるが、getHogehogeの完了を待機するスレッドはブロックしない。
メインスレッドがブロックするとANRの原因となるが、このコードでメインスレッドはブロックしない。
コールバック地獄が発生しない
前項に書いた通り、スレッドの管理は自力でしたくない。
コルーチンがない環境でスレッドの管理をライブラリ側に押し付けるために使われていた手法がコールバックだった。
処理に時間がかかる関数は引数として「処理が完了したときに実行する関数」を受け取るようにしておく、というもの。
fun getHogehoge(onComplete: (Hogehoge) -> Unit)
getHogehoge { hogehoge ->
view.updateHogehoge(hogehoge)
}
getHogehogeそのものは即returnする。後にgetHogehogeに渡したラムダ式が実行される。
だから処理中にメインスレッドはブロックしないし、完了後の処理はメインスレッドで行えるというワケ。
そしてこの手法を使ったときに発生していたのがコールバック地獄である。
getHogehoge { hogehoge ->
view.updateHogehoge(hogehoge)
getPiyopiyo { piyopiyo ->
view.updatePiyopiyo(piyopiyo)
getFugafuga { fugafuga ->
view.updateFugafuga(fugafuga)
}
}
}
コルーチンが導入されている環境ではコールバックを使う必要はない。
もし古きライブラリを使うことになり、コールバック形式の関数しか用意されておらず辛くなったときはsuspendCancellableCoroutine
を使うといい。
suspend fun getHogehogeSuspend(): Hogehoge {
return suspendCancellableCoroutine { continuation ->
getHogehoge { hogehoge ->
continuation.resume(hogehoge)
}
}
}
suspendCoroutine
という関数もあるが、こちらはkotlin.coroutines(xがつかない方)のパッケージにあり、その名の通りキャンセルをサポートしていない。キャンセルはあくまでkotlinx.coroutinesというライブラリが用意した機能であり、suspend funの言語仕様には含まれていない。
if, tryなどの構文をそのまま利用できる
suspend funはコンパイル後は複雑なフローに変換されるが、我々がコードを記述するときには通常通りifなどの言語仕様をフルに利用できる。
launch {
val hogehoge = getHogehoge()
if (hogehoge.isEnabled) {
val piyopiyo = getPiyopiyo()
view.updatePiyopiyo(piyopiyo)
} else {
val fugafuga = getFugafuga()
view.updateFugafuga(fugafuga)
}
}
Executorを使う場合
executor.execute {
val hogehoge = getHogehoge()
if (hogehoge.isEnabled) {
val piyopiyo = getPiyopiyo()
handler.post {
view.updatePiyopiyo(piyopiyo)
}
} else {
val fugafuga = getFugafuga()
handler.post {
view.updateFugafuga(fugafuga)
}
}
}
コールバック方式の場合
getHogehoge { hogehoge ->
if (hogehoge.isEnabled) {
getPiyopiyo { piyopiyo ->
view.updatePiyopiyo(piyopiyo)
}
} else {
getFugafuga { fugafuga ->
view.updateFugafuga(fugafuga)
}
}
}
launch {
val hogehoge = getHogehoge()
val hogera = if (hogehoge.isEnabled) {
getPiyopiyo()
} else {
getFugafuga()
}
view.updateHogera(hogera)
}
Executorを使う場合
executor.execute {
val hogehoge = getHogehoge()
val hogera = if (hogehoge.isEnabled) {
getPiyopiyo()
} else {
getFugafuga()
}
handler.post {
view.updateHogera(hogera)
}
}
コールバック方式の場合
getHogehoge { hogehoge ->
if (hogehoge.isEnabled) {
getPiyopiyo { piyopiyo ->
view.updateHogera(hogera)
}
} else {
getFugafuga { fugafuga ->
view.updateHogera(hogera)
}
}
}
launch {
showLoadingIndicator()
val hogehoge = try {
getHogehoge()
} catch (e: CancellationException) {
throw e
} catch (e: IOException) {
showIOErrorDialog()
return@launch
} catch (e: Exception) {
showErrorDialog()
return@launch
} finally {
hideLoadingIndicator()
}
view.updateHogehoge(hogehoge)
}
Executorを使う場合
executor.execute {
handler.post {
showLoadingIndicator()
}
val hogehoge = try {
getHogehoge()
} catch (e: InterruptedException) {
throw e
} catch (e: IOException) {
handler.post {
showIOErrorDialog()
}
return@execute
} catch (e: Exception) {
handler.post {
showErrorDialog()
}
return@execute
} finally {
handler.post {
hideLoadingIndicator()
}
}
handler.post {
view.updateHogehoge(hogehoge)
}
}
コールバック方式の場合
showLoadingIndicator()
getHogehoge(
onSuccess = { hogehoge ->
hideLoadingIndicator()
view.updateHogehoge(hogehoge)
},
onError = { e ->
if (e is IOException) {
showIOErrorDialog()
} else {
showErrorDialog()
}
hideLoadingIndicator()
}
)
launch {
val hogeList = getHogeList()
val piyoList = hogeList.map { getPiyo(it) }
for (piyo in piyoList) {
view.updatePiyo(piyo)
}
}
Executorを使う場合
executor.execute {
val hogeList = getHogeList()
val piyoList = hogeList.map { getPiyo(it) }
handler.post {
for (piyo in piyoList) {
view.updatePiyo(piyo)
}
}
}
コールバック方式の場合
とりあえず今思いついたのはこの方法。もうちょいマシな方法もあるかもしれん。
getHogeList { hogeList ->
mapHogeList(hogeList, emptyList())
}
fun mapHogeList(hogeList: List<Hoge>, piyoList: List<Piyo>) {
if (hogeList.isNotEmpty()) {
getPiyo(hogeList.first()) { piyo ->
mapHogeList(hogeList.drop(1), piyoList + piyo)
}
} else {
handler.post {
for (piyo in piyoList) {
view.updatePiyo(piyo)
}
}
}
}
このデメリットがあまりにもデカすぎるので、私はスレッドの管理を自力でするほうがまだマシだとずっと思っていた。今は昔の話だが。
終わりに
いかがでしたか?(追い打ち)
今一度kotlinx.coroutinesのありがたみを再確認して、この強力なライブラリを正しく最大限に利用していきたいですね。