Kenji Abe

Photo by Chris Lawton on Unsplash

Navigation Composeでも NavOptions が設定でき、画面遷移を細かく制御できるようになっています。この NavOptions の挙動についてまとめておきます。

NavGraph

今回使用するNavGraphの定義は以下のようなシンプルな構成を考えます。

launchSingleTop

オプションなしの画面遷移では、バックスタックのトップにある(今表示されてる)画面から同じ画面に遷移した場合、同じ画面が上に積まれることになります。遷移後にバックキーを押すと同じ画面がまた表示される感じです。

navController.navigate("screen_a")

--

--

Kotlinの data classcopy を使うときに気をつけないと意図しないデータ不整合が起きる可能性があります。Coroutinesを使ってると陥りやすい罠かなと思います。

現象

想定としてAndroidのComposeを考えます。他のケースでも同様です。

以下のような、Coroutinesを使ってユーザー一覧を別スレッドで取得する処理があるとします。

この処理をViewModelで以下のように取得し、UiStateを copy を使って更新するとします。また、UiStateの別プロパティの取得処理もするとします。

この状態で、ComposeのコードからViewModelの処理を連続で呼び出します。

期待する結果としては、ユーザー一覧とフラグtrueがUiStateに設定されてることだと思います。

しかし、結果は以下のように想定しないものになります。

uiState = UiState(users=[], flag=true)  <- フラグ更新後uiState = UiState(users=[User(...)], flag=false) <- ユーザー一覧取得後

最初に、フラグを更新結果がログに出力されて、次にユーザー一覧取得後の結果がログに出力されています。

しか、フラグは最初は正しいですが、ユーザー一覧取得後には元に戻ってしまっています。

原因

原因としては ユーザー一覧の取得処理です。ここでは copy 処理の中でCoroutinesの別スレッドの処理を呼び出しています。

そのため、処理前のUiStateの状態をキャプチャして、別スレッドの処理が完了後にその状態をコピーするようになっています。

そのため、この別スレッドの処理の間に何か状態が変更されたとしても、反映されないため巻き戻ってしまうことになります。

解決方法

この解決方法はいたって簡単です。 copy 処理の中で別スレッド処理をしないようにするだけです。なので、copy 前に処理するだけです。

これだけで大丈夫です。

Coroutinesが別スレッドで実行されてることを意識せずコードを書けるの非常に便利ですが、こういった罠があるため注意が必要です。

--

--

よくあるケースとして、別のActivityを起動して今開いてるActivityを閉じるというケースがあると思います。実はこのときに startActivityfinish を呼び出す順番で挙動が異なります。

先に startActivity を呼び出して次に finish を呼び出す場合は基本的に意図しない挙動にはなりません。

startActiv …

--

--

Photo by Possessed Photography on Unsplash

Lifecycle ViewModel 2.5.0-alpha03 で InitializerViewModelFactory が追加されています。この InitializerViewModelFactory を使ってViewModelを生成する方法を紹介します。

ここでは CreationsExtras についても出てくるので、前回書いた記事を参考にしてください。

InitializerViewModelFactory を使うとシンプルにViewModelFactoryを生成することができるようになっています。

※これを書いてる時点ではalphaなので今後変更があるかもです。

環境

  • androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-alpha03
  • androidx.fragment:fragment-ktx:1.5.0-alpha03

Composeの場合

  • androidx.activity:activity-compose:1.5.0-alpha03
  • androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha03

簡単な使用例

例としてコンストラクタ引数を一つ持ったViewModelを生成してみます。Activityから生成することを想定します。

InitializerViewModelFactory は直接使用することはできないため、 InitializerViewModelFactoryBuilder を使って InitializerViewModelFactory を作成します。

addInitializer の中で単純にViewModelのインスタンスを作っています。このとき引数も渡しています。

あとはこのFactoryを ViewModelProvider を使ってViewModelを取得するだけです。

全体的なコードは以下のような感じになります。

viewModelFactory関数

viewModelFactory という関数が用意されてるので、そちらを使うともう少し簡単にFactoryを作れます。

このFactoryは複数のViewModelにも対応できます。

--

--

Photo by israel palacio on Unsplash

Navigation Composeの引数にて単純にParcelableのような複雑なデータを扱うことはできないですが、とはいえ次の画面に複雑なデータを渡したいことはよくあります。
そういう場合の実装方法について3パターン紹介します。

データレイヤーで解決する

まず1つ目の方法ですが、Androidチームが推奨している方法になります。画面遷移ではデータ自体を渡すのではなく、データを参照できるようなIDなどを渡して取得する感じですね。

ドキュメントには複雑なデータを渡すのはアンチパターンとして記載されています。(とはいえ、データ渡したいですよね…)

Caution: Passing complex data structures over arguments is considered an anti-pattern. Each destination should be responsible for loading UI data based on the minimum necessary information, such as item IDs. This simplifies process recreation and avoids potential data inconsistencies.

例として一覧画面から詳細画面を表示するようなケースですは、一覧からはIDを渡して、詳細画面でそのIDを使ってデータを再取得する感じになります。

IDで取得するようなAPIが提供されていない場合などで単純に再取得が出来ない場合は、メモリキャッシュなどを活用する必要があります。

設計ガイドでメモリキャッシュについての実装があるのでそちらも参考にする良いと思います。(ドキュメント

共有ViewModelを使う

ViewModelをComposeの画面間で共有することで、データを受け渡す方法です。

ViewModelを画面間で共有するには親となるViewModelStoreOwnerを指定する必要があります。以下のような関数を作っておくと良いと思います。

あとは、ViewModelStoreOwner指定すればViewModelを共有することが可能です。

この実装での問題点としては共有するViewModelが生存期間が長くなり、使用しない画面でも生存している状態になります。

この生存期間が長い問題は、NavGraphをネストすることで対応することができます。

この実装については、StackOverflowにてGoogleのIan氏の回答を参考にしています。

カスタムNavTypeを使う

最後に紹介する方法として、カスタムNavTypeを使うパターンです。

まずは以下のような感じでカスタムNavTypeを定義します。この例では実態としてJSONを使って受け渡す感じになります。

次にNavGraphの定義と値を渡す側の実装です。

--

--

Photo by Lisa Luminaire on Unsplash

Androidの公式ドキュメントにある設計ガイドが更新され、実装について以前よりだいぶ明確に指針が記載されています。

その中で、ViewModelからのイベントで画面にメッセージを表示するような消費型のイベントについての記載があります。

https://developer.android.com/jetpack/guide/ui-layer/events#consuming-trigger-updates

イベントも状態として表現しており、メッセージ等を表示後にその状態を更新するような感じの処理になります。

個人的にずっと悩んでた、SingleLiveEventは推奨されないけどどう実装するのが良いのだろう…っていう悩みが解決しました。

ただ、ガイドでは簡略されてるため実際にこのパターンで実装するうえでの悩みと実際にどう実装すれば良いかを考えてみました。

実装の悩みどころ

設計ガイドの実装では Snackbar でメッセージを表示するようになっていますが、現実ではそれ以外にも処理したいことがあります。

例えば、何か処理が終わって成功したときに画面遷移をしたい場合にどう実装するのが良いのか。

メッセージを表示する手段も、 Snackbar だけじゃなく AlertDialog や Toast もあります。

この複数の種類があるイベント処理についてどう実装するのが良いのかが悩みどころです。

これをどう解決するかを考えてみました。

UIの処理パターンで状態を分ける

例えば、以下のような感じで、UIでの処理パターンを状態としてUI Stateで別々で管理する方法です。

更にViewModel側でこれらのイベントを消費するメソッドを用意してあげます。実装例は省略してますが、それぞれ処理済みのイベントListを除外してUI Stateを更新する感じです。

ViewModelからイベントを発行するには以下のような感じです。

UI側はそれぞれで対応する処理をする感じです。以下の例はComposeを想定していますが、Viewシステムで似たような感じになります。

実装としてはこれでも問題なく実現できるかなぁとは思いますが、だいぶ冗長な感じがしています。UIの処理パターンが増えるたびに管理する状態も増えていきます。

また、ViewModelからイベントを発行する際にUIの処理を強く意識する必要があります。

イベントを1つの状態で管理する

ぼくが考えたもう一つの実装方法として、イベントを1つの状態として管理する方法です。

まず、ViewModelから発生するイベントを sealed interface/class で定義し、UI Stateで管理します。この例では成功とエラーの2種類を定義しています。

次にイベント処理後に消費するためのメソッドを追加します。単純にイベントListから消費したいイベントを除外したListを作成してUI Stateを更新する感じですね。

--

--

Kenji Abe

Kenji Abe

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO