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 を呼び出す場合は基本的に意図しない挙動にはなりません。

startActivity(intent)
finish()

逆に先に finish を 呼び出して startActivity を呼び出す場合は、実は意図しない挙動になっています。

finish()
startActivity(intent)

このとき、ActivityのIntentには FLAG_ACTIVITY_NEW_TASK が自動で追加されています。

呼び出し先のActivityに以下のようなコードを書いておくと確認できます。

この挙動が意図しない結果になる可能性があるため注意が必要です。

基本的に startActivity を呼び出してから finish を呼び出すようにしたほうが安全だと思います。

参考

--

--

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 Alex Shute on Unsplash

Lifecycle ViewModel 2.5.0-alpha01 に CreationExtras が追加されました。この CreationExtras について使い方などを紹介したいと思います。

CreationExtrasViewModelProvider.Factory に追加の情報として渡すことのできるMapのようなものになっています。また、 ViewModelProvider.Factory が状態を持つことなく値を渡すことが可能なります。

※これを書いてる時点では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

使い方

Activityで以下のようなViewModelを生成する例です。

単純な引数を渡す方法は以下のような感じになります。ちょっと長いですが。

CreationExtras.Key

CreationExtras.KeyCreationExtras で使用するKeyになります。型パラメータの型が実際に設定したい型になります。この場合はIntになります。

CreationExtrasの作成

CreationExtras を作成して値を設定するには MutableCreationExtras を使用します。Keyには CreationExtras.Key のものを指定します。

MutableCreationExtras のコンストラクタにはマージしたい別の CreationExtras を指定できます。

コード例で CreationExtras 作成時に指定してる defaultViewModelCreationExtras はデフォルトで様々な値が設定されています。これを含めるために MutableCreationExtras のコンストラクタに渡しています。 defaultViewModelCreationExtras はまた後述します。

ViewModelProvider.Factory

ViewModelProvider.Factory は新しく CreationExtras を受け取る create メソッドが追加されいます。これを使って渡された値を取得できますので、それを使ってViewModelを作成します。

--

--

Photo by Denise Jans on Unsplash

Navigation Composeはそのまま使うとコード量が増えたり引数の扱いが面倒だったりします。こういった問題を便利にしてくれる compose-destinations というライブラリを紹介します。

このライブラリはNavGraphの設定を簡単にしてくれたり、Type-safe引数を実現してくれます。このライブラリはKSPを使ったコード生成をしています。

これを書いてる時点では 1.2.1-beta です。今後変更があるかもしれません。

簡単な使い方

まずは、簡単な使い方を紹介します。単純に次の画面に遷移するだけのものです。セットアップはREADME等を見てください。

最初に画面のトップとなるComposable関数に @Destination をつけて必要な実装していきます。簡単な説明はコード上にコメントしています。

最後にNavGraphを設定しますが、これはライブラリが生成した NavGraphsDestinationsNavHost に設定するだけです。

これだけで画面遷移の実装ができます。

引数を扱う

引数も簡単に扱うことができます。

受けとりたい引数をComposable関数の引数として指定するだけです。あとはコード生成で引数が渡されるDestinationが生成されるので、それに渡すだけです。

Parcelableの引数

Parcelableの引数も先程と同じように簡単に扱うことが可能になっています。

これはParcelableをBase64に変換することで実現しています。正直、Base64にして扱って良いものかは分からないのですが、ぼくが触ってる感じパフォーマンスが悪いとかはなかったです。

NavBackStackEntryなども引数に指定するだけで受け取れます。

その他

他にもアニメーションの対応や、NavGraphを細かく自分で設定できたりもします。

詳しくは Wiki 等を見てもらえると。

まとめ

compose-destinations について簡単に紹介しました。

個人的には便利に使えそうだなぁって思っています。今のところIssueなどの対応も早いので、なにか気になることあればIssueをあげると良いかなと。

あと個人的に気にしてることですが、諸事情で後から捨てるとなったとしても、なんとか対応できそうな感じなんで、とりあえず試すのもアリかなぁって思ってます。

気になる人は試してみると良いと思います。

--

--

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