この記事は、Money Forward Engineering 2 Advent Calendar 2023、22日目の投稿です。
こんにちは、マネーフォワードホームカンパニーでモバイルエンジニアをやっている nyafunta9858 です。
この記事では、Android Jetpackで提供されているAPIのひとつ、repeatOnLifecycle
についてアンチパターンから理解を深めてみたいと思います。
実はこのテーマは、DroidKaigi 2023にて筆者が出題したコードクイズのテーマのひとつでもあったので、その際のクイズを題材にして解説していきます。
年末に向けて忙しくされている皆さまの頭の体操として、まだご覧になっていないかたも、ぜひコーヒーを片手にお楽しみいただけますと幸いです。
さて問題です
以下がDroidKaigi 2023のスポンサーブースで出題した問題になります。
モバイルプラットフォームであるAndroidらしい題材として、ライフサイクル及びKotlinのCoroutinesを採用しています。
※出展当時の問題はこちら
twitter.com午後の問題に切り替えました🎉ぜひまたチャレンジしにブースお越しください🙌この問題は @nyafunta9858 がつくりました🍊 #DroidKaigi pic.twitter.com/U5RTwJR6Gc
— Money Forward Developers (@moneyforwardDev) 2023年9月14日
補足しますと、操作手順の3番目を実行して以降に出力されるログの内容がどうなるか?を解答する問題になっています。つまり、操作手順の1.の時点で起動されるCoroutineからログ出力が行われますが、これらは解答には含みません。
解説、のその前に
早速解説をしていきたいところですが、その前に、回答する際に押さえておきたいAPIについて少し復習したいと思います。
lifecycleScope.launch
Coroutine(コルーチン)はCoroutineScope
という世界の中でlaunch
などのCoroutine Builderと呼ばれるAPIを用いて起動することができます。また、特定のライフサイクルに合わせてCoroutineをキャンセルさせることも可能です。問題の中では、Activity
のライフサイクルに合わせてキャンセルされるlifecycleScope
を使ってCoroutineを起動しています。
repeatOnLifecycle
タイトルにもある通り、本記事の重要人物(API)です。
ライフサイクルを考慮した非同期処理のキャンセル・起動をシンプルに実現することは、長い間、多くのAndroidエンジニアにとって課題だったんじゃないかなと思います。
repeatOnLifecycle
は指定したライフサイクルに満たない状態のときにはCoroutineがキャンセルされ、指定したライフサイクルを満たしたタイミングでCoroutineを起動するため、シンプルで可読性を保証しやすい非同期処理を実現することができます。
また、このAPIのAPI Docsには、ベストプラクティスや取り扱いには一定の注意が必要であることが記載されています。
The best practice is to call this function when the lifecycle is initialized. For example, onCreate in an Activity, or onViewCreated in a Fragment. Otherwise, multiple repeating coroutines doing the same could be created and be executed at the same time.
要するに、「繰り返し起動されるCoroutineが多重起動されてしまう可能性があるため、repeatOnLifecycle
を起動するCoroutineは一度だけ起動されるようにする必要がある」ということが言われています。
今回の問題では、まさにこのアンチパターンの際の挙動を考えるものになっているわけです。
解説
それでは解説していきます。ここからが本編です。 この問題を考えるにあたって、以下の点を押さえられると挙動を捉えやすいのではないかと思います。
押さえておきたいポイント
repeatOnLifecycle
を起動するCoroutineが起動される場所- 操作手順3.実行前までに起動されるCoroutine
- 操作手順3.実行してから起動されるCoroutine
- 上記のCoroutineのうち、操作手順3.を実行したあと、通算2度目の
onResume
が「呼ばれる前」と「呼ばれている間」にrepeatOnLifecycle
のCoroutineが起動されるのはどれか?
まずは、repeatOnLifecycle
を呼び出すCoroutineがどのようなタイミングで起動されているか見てみましょう。先述の説明から、Activity
内で呼ぶのであればActivity
が作られたときに呼び出されるonCreate
で呼ぶのがベストプラクティスですが、今回はActivity
が再び表示されたときに呼び出されるonResume
で呼ばれています。
そのため、このActivity
から離れたあとに再度戻ってくるとその都度、repeatOnLifecycle
を起動するためのCoroutineが起動され、repeatOnLifecycle
が重複していくことが分かります。まさにアンチパターンとして記載されている通りの振る舞いになるわけです。
実は、このアンチパターンの実装をしようとするとIDEから警告されビルドができなくなるのですが、それを回避するためにRepeatOnLifecycleWrongUsage
という指摘をSuppressLint
で無視するようにしています。もしかすると回答されたかたの中には、ベストプラクティスを知らなくてもRepeatOnLifecycleWrongUsage
という名前から推測されたというかたがいらっしゃったかもしれないですね。
次に押さえておきたいのはポイントの2つ目、3つ目にある、操作手順3.実行前後で起動されるCoroutineです。
操作手順1.から3.までの操作を行なうと、onResume
が2回呼ばれることは自明かと思いますが、そうするとrepeatOnLifecycle
を起動するCoroutineもそれぞれ2回起動されることが分かります。
つまり、「ライフサイクルごとに2個ずつ起動されるrepeatOnLifecycle
のうち、操作手順3.を実行したときにどのCoroutineが起動され、その順番はどうなるか?」が、この問題が求めるより具体的なポイントになります。
そして最後のポイント、どのrepeatOnLifecycle
のCoroutineが起動されるか?です。
ここでもonResume
に注目したいのですが、今回repeatOnLifecycle
で指定しているLifecycle.State
のうち、Lifecycle.State.CREATED
とLifecycle.State.STARTED
はonResume
より前に来るライフサイクルのイベントです。そうなんです、操作手順3.をすると、2回目のonResume
が呼ばれる前にこれらのライフサイクルを通る可能性があるんです。
ただし今回はアプリを終了せずに再開しているため、Activity
破棄時のライフサイクルイベント、onDestroy
は呼ばれないため、1回目に起動されたrepeatOnLifecycle
のうちLifecycle.State.CREATED
を指定したもののCoroutineは起動されず、Lifecycle.State.STARTED
を指定したほうのCoroutineだけが起動されます。この時点ではまだonResume
の処理は終わっていないため、Lifecycle.State.RESUMED
を指定したrepeatOnLifecycle
からのCoroutineはまだ起動・実行はされませんが、onResume
の終わりには呼び出されることになります。
ここまでを整理すると以下のようになります。
表1. 操作手順3.のあと、onResumeの前に呼び出されるrepeatOnLifecycle
CREATED | STARTED | RESUMED |
---|---|---|
✕ | ◯ | - (呼び出し前) |
次に、onResume
の中のrepeatOnLifecycle
を起動する各Coroutineが起動され、その中で各Lifecycle.State
のrepeatOnLifecycle
がそれぞれ起動されることに加えて、保留されていた1回目に起動されたLifecycle.State.RESUMED
のrepeatOnLifecycle
も起動されます。
これらの起動されるすべてのrepeatOnLifecycle
をまとめると以下の表になります。
表2. 操作手順3.のあとに呼び出されるrepeatOnLifecycle
CREATED | STARTED | RESUMED | |
---|---|---|---|
1回目のonResumeで起動された repeatOnLifecycleのうち 操作手順3.で起動されるもの |
✕ | ◯ | ◯ (ただしonResume後に呼ばれる) |
2回目のonResumeで起動される repeatOnLifecycle |
◯ | ◯ | ◯ |
これらを1回目のonResume
で起動されたrepeatOnLifecycle
のうちLifecycle.State.RESUMED
だけ後回しにされるのを考慮すると、以下の順番でログが出力されることになります。
- Lifecycle.State.STARTED:操作手順1.で起動されたrepeatOnLifecycle(Lifecycle.State.STARTED)のログ
- Lifecycle.State.CREATED:操作手順3.で起動されたrepeatOnLifecycle(Lifecycle.State.CREATED)のログ
- Lifecycle.State.STARTED:操作手順3.で起動されたrepeatOnLifecycle(Lifecycle.State.STARTED)のログ
- Lifecycle.State.RESUMED:操作手順1.で起動されたrepeatOnLifecycle(Lifecycle.State.RESUMED)のログ
- Lifecycle.State.RESUMED:操作手順3.で起動されたrepeatOnLifecycle(Lifecycle.State.RESUMED)のログ
そのため選択肢の3番が正解になります。
おわり
いかがでしたでしょうか?お手元のコーヒーと共にご堪能いただけましたでしょうか? あえてアンチパターンを踏んでみることで、ベストプラクティスの背景を深く理解することができることもあると思い、今回のようなテーマにしてみました。 今後もベストプラクティスをただ使うのではなくて、その裏側にも思いを馳せながら理解を深めて、この便利な機能を活用したいと思っている次第です。
もし来年もコードクイズで出展することがあれば、出題者に名を連ねたいと思いますので、その際はまたいろいろとお話しできることを楽しみにしております。