Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

アンチパターンから理解を深めるrepeatOnLifecycle

この記事は、Money Forward Engineering 2 Advent Calendar 2023、22日目の投稿です。

こんにちは、マネーフォワードホームカンパニーでモバイルエンジニアをやっている nyafunta9858 です。

この記事では、Android Jetpackで提供されているAPIのひとつ、repeatOnLifecycleについてアンチパターンから理解を深めてみたいと思います。 実はこのテーマは、DroidKaigi 2023にて筆者が出題したコードクイズのテーマのひとつでもあったので、その際のクイズを題材にして解説していきます。 年末に向けて忙しくされている皆さまの頭の体操として、まだご覧になっていないかたも、ぜひコーヒーを片手にお楽しみいただけますと幸いです。

さて問題です

以下がDroidKaigi 2023のスポンサーブースで出題した問題になります。

moneyforward-dev.jp

moneyforward-dev.jp

モバイルプラットフォームであるAndroidらしい題材として、ライフサイクル及びKotlinのCoroutinesを採用しています。

※出展当時の問題はこちら

twitter.com

補足しますと、操作手順の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.CREATEDLifecycle.State.STARTEDonResumeより前に来るライフサイクルのイベントです。そうなんです、操作手順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.StaterepeatOnLifecycleがそれぞれ起動されることに加えて、保留されていた1回目に起動されたLifecycle.State.RESUMEDrepeatOnLifecycleも起動されます。 これらの起動されるすべてのrepeatOnLifecycleをまとめると以下の表になります。

表2. 操作手順3.のあとに呼び出されるrepeatOnLifecycle

CREATED STARTED RESUMED
1回目のonResumeで起動された
repeatOnLifecycleのうち
操作手順3.で起動されるもの

(ただしonResume後に呼ばれる)
2回目のonResumeで起動される
repeatOnLifecycle

これらを1回目のonResumeで起動されたrepeatOnLifecycleのうちLifecycle.State.RESUMEDだけ後回しにされるのを考慮すると、以下の順番でログが出力されることになります。

  1. Lifecycle.State.STARTED:操作手順1.で起動されたrepeatOnLifecycle(Lifecycle.State.STARTED)のログ
  2. Lifecycle.State.CREATED:操作手順3.で起動されたrepeatOnLifecycle(Lifecycle.State.CREATED)のログ
  3. Lifecycle.State.STARTED:操作手順3.で起動されたrepeatOnLifecycle(Lifecycle.State.STARTED)のログ
  4. Lifecycle.State.RESUMED:操作手順1.で起動されたrepeatOnLifecycle(Lifecycle.State.RESUMED)のログ
  5. Lifecycle.State.RESUMED:操作手順3.で起動されたrepeatOnLifecycle(Lifecycle.State.RESUMED)のログ

そのため選択肢の3番が正解になります。

おわり

いかがでしたでしょうか?お手元のコーヒーと共にご堪能いただけましたでしょうか? あえてアンチパターンを踏んでみることで、ベストプラクティスの背景を深く理解することができることもあると思い、今回のようなテーマにしてみました。 今後もベストプラクティスをただ使うのではなくて、その裏側にも思いを馳せながら理解を深めて、この便利な機能を活用したいと思っている次第です。

もし来年もコードクイズで出展することがあれば、出題者に名を連ねたいと思いますので、その際はまたいろいろとお話しできることを楽しみにしております。