Androidのライフサイクルとコルーチンの話

(趣味の)Androidアプリ開発でライフサイクルに苦しみ、とくに非同期処理周りの設計について考えさせられた。そのうち忘れそうなので、色々と考えたことをまとめておきたい。

Kotlinのコルーチンで最も特徴的なのは、スコープの存在だと思う。コルーチンはスコープと紐づけて実行し、スコープが破棄されると強制的にキャンセルされる。プロセスが死ぬとスレッドが停止するのと似ているが、プロセスが死んだあとのことを考える機会は少ない。コルーチンを使う場合はスコープを意識したい。

コルーチンの処理を実際に走らせるのはスレッドである。アプリケーションを実行しているMainスレッドと、複数のWorkerスレッドがある。どのスレッドでコルーチンを実行するかは、コルーチン開始時に指定できる。(備考: イベントループがない場合の挙動が気になったが、Dispatcher.Mainが必ず存在するとは限らない。)

AndroidのActivityやFragmentの寿命はかなり特殊で、プロセスは生きているのに突然UIが再生成されることがある。Mainスレッドは生きているので、スレッドに登録した仕事は完遂するが、それまで参照していたActivityが無くなっているかもしれない。たとえGCで回収されいなくても、その裏で必要な資源は解放されているかもしれない。

コルーチンのいい所は、スコープが破棄されるタイミングで全てのコルーチンがキャンセルされる点にある。ActivityやFragmentはLifecycleScopeを持つので、これを使えばUIを更新する処理でActivityの状態を気にする必要がなくなる。

ただ、コルーチンがスコープによってキャンセルされるということは、実行保証がないことも意味する。重要なイベントが発生したが、画面を回転したばっかりに処理が走らなかった・・・ということはできれば避けたい。LifecycleScopeではなくCoroutineScopeを使えばキャンセルされることはなくなるが、今度はActivityの状態に気を付けないといけない。このように、ドメインロジックをActivityの上で動かすのは意外と悩ましいのだ。

同じ悩みはEventBusのようなメッセージ機構を使った場合でも起こる。ActivityはEventBusからUnsubscribeすることで受信対象から外れる。メッセージは届けられるが、聞き手がいなくなってしまう。

確実に動作させたい処理はCoroutineScopeを使い、Activityの状態に気を付ける。そうでなければLifecycleScopeが便利。

1. 処理がUIを更新するためのもの

LifecycleScopeに紐づけると楽

2. 処理がデータ永続化に関するもの

CoroutineScopeで確実に実行する

3. 永続化した後、UIを更新したい

CoroutineScopeで実行する。完了後、MainThreadにメッセージを投げてUIからデータを読込むか、コルーチンをMainThread上で再Launchする。後者の場合、Activityの状態に気をつける。

4. 複数の処理をキューするもの

重要なら永続化されたキューに、重要でなければ破棄できるキューに追加する。キューから仕事を取り出すスレッドまたはコルーチンが必要。(データが来るまでコルーチンをsuspendするキューをchannelという。)

このように非同期処理の性質を踏まえて、正しい呼び出し方を選ぶのは意外と面倒です。

あと、結果が返ってきていないコルーチンはいつでもキャンセルされると思うこと。トランザクションはデータベース操作にはあるが、データ操作にはない。

DDDでは集約がトランザクション境界であり、複数の集約を永続化したときの一貫性は保証されない。DDDで集約をImmutableにする制約はない。異常が発生したら読み込み直す意図があるのかもしれない