意外と知られていない気がするので、ViewModelで画面引数を受け取る方法をまとめておきます。
AssistedInjectなどの特殊なことも必要なく、かなり簡単にViewModelで画面引数を受け取れます。
受け取る方法
実はViewModelで画面引数を受け取るには非常に簡単で、以下のように SavedStateHandle
から取得できます。
class SampleViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId: String? = savedStateHandle["userId"]
// ...
}
ViewModelのコンストラクタで SavedStateHandle
を受け取るようにしておくと、画面引数として渡したときと同じキーを使うだけ取得できます。
渡す方法
画面引数を渡す方法に特別なことは不要で、ActivityのIntent、Navigation Component、Navigation Compose、どの渡し方でも SavedStateHandle
で取得することができます。
// Activity Intent
val intent = Intent(this, SampleActivity::class.java).apply {
putExtra("userId", "xxxx")
}
startActivity(intent)
// Navigation Component
val action = SampleFragmentDirections.action("xxxx")
findNavController().navigate(action)
// Navigation Compose
NavHost(navController = navController, startDestination = "start") {
composable("start") {
HomeScreen(
onClick = { navController.navigate("sample/xxxx") }
)
}
composable("sample/{userId}") {
// ...
}
}
Safe Args対応
Navigation Componentを使ってるときにSafe Argsを使うことが多いと思いますが、SavedStateHandle
から引数を取得するときにもSafe Argsのクラスに変換することが可能です。 fromSavedStateHandle
というヘルパーメソッドが生成されるので、それを使うだけで簡単に対応することができます。
class SampleViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
val args = SampleFragmentArgs.fromSavedStateHandle(savedStateHandle)
// ...
}
Navigation ComposeのType Safe対応
Navigation Composeを使ってる場合は自分でWrapperを実装する感じになります。詳細は以下の公式ドキュメントにまとまっています。実装自体は単純です。
Composeの画面遷移で、前の画面に結果を返す方法についてまとめておきます。
基本的にはFragmentと同じような感じで NavBackStackEntry
の SavedStateHandle
を使用していきます。
結果を返す
ScreenAからScreenBに遷移して、ScreenBでの画面の結果をScreenAに返す処理です。
navController.previousBackStackEntry?.run {
savedStateHandle["result"] = "Foo"
}
上記のように NavHostController#previousBackStackEntry
の SavedStateHandle
にKey-Valueで設定するだけです。
previousBackStackEntry
っていうのが大事で前の画面の NavBackStackEntry
アクセスしています。
結果を受け取る
今度はScreenA側でScreenBの結果を受け取る方法です。
val result: String? = navController
.currentBackStackEntry?.savedStateHandle?.get("result")
受け取るには NavHostController#currentBackStackEntry
の SavedStateHandle
から取得することが出来ます。
受け取るときは currentBackStackEntry
を使います。
結果の削除
結果を受け取るときの注意としては、基本的にセットした結果は自分で削除しない限りは残り続けます。画面回転等でも残ります。
そのため、結果がセットされた後にRecompositionが行われると何度も結果を取得することが出来ます。
UI状態して同じ結果が返りつづけるのを期待している場合は問題ないかもですが、もし、結果によって一度だけ処理したい場合は対応が必要になります。例えば、結果を受け取ったら画面をリフレッシュするとかの場合です。
ワンショットの処理が行いたい場合は以下のように処理後に remove
で削除することで何度も呼び出されることはなくなります。
navController.currentBackStackEntry?.run {
if (savedStateHandle.contains("result")) { // 結果があるか確認
// 結果取得
val result: String? = savedStateHandle["result"]
// 結果を使った処理
viewModel.someAction(result)
// 結果を削除
savedStateHandle.remove<String>("result")
}
}
また、 remove
は削除前にセットされてる値を返すので、以下のような書き方もで結果の取得と同時に削除することもできます。
val result: String? = navController
.currentBackStackEntry?.savedStateHandle?.remove("result")
ただ、個人的には確実に処理後に結果を削除したほうが良いと思うので、前者の実装のほうが良いかなと思います。
Recompositionへの対応
前述までの方法でも問題ないのですが、ComposeはRecompositionにより何度も処理が実行される可能性があります。
そのため、これまでの実装だとRecompositionごとに画面の結果があるかを確認することになります。
LaunchedEffect
等で一度だけ実行されるようにしておくと良いと思います。
LaunchedEffect(Unit) {
navController.currentBackStackEntry?.run {
if (savedStateHandle.contains("result")) {
// ...
}
}
}
参考
Accompanist
に adaptive
というライブラリが追加されました。これを使用して2ペインのレイアウトを組むことができます。この実装方法について解説していきます。
注意: AccompanistはExperimentalなものなので今後変更される可能性があります。
実装方法
build.gradle
は accompanist-adaptive
を追加します。
dependencies {
implementation "com.google.accompanist:accompanist-adaptive:<version>"
}
単純な半分で左右に分割する実装です。
TwoPane
のComposable関数を使っていきます。
first
は左側の部分、 second
は右側の部分の実装になります。この引数に表示したいComposable関数を記述します。
strategy
は後で解説しますが、 TwoPaneStrategy
を指定します。ここでは左右に半分に分割する指定になります。
displayFeatures
は後で解説します。
これを実行すると以下のようになります。
Composeにおいてrecomposition後も状態を保持するために remember
がありますが、画面回転などConfiguration Change後でも状態を保持する rememberSaveable
について解説します。
rememberSaveable
単純な値で rememberSaveable
を使う場合は remember
とほぼ変わりません。
また、 mutableStateOf
も同様に使用できます。
これだけでConfiguration Change後も値を復元してくれます。
使用できる型
rememberSaveable
はすべての型が使用できるわけではありません。
例えば、上記のように独自のクラスを rememberSaveable
で使おうとすると、以下の例外を投げます。
java.lang.IllegalArgumentException: User(name=) cannot be saved using the current SaveableStateRegistry. The default implementation only supports types which can be stored inside the Bundle. Please consider implementing a custom Saver for this class and pass it to rememberSaveable().
rememberSaveable
は最終的に ComponentActivity#onSaveInstanceState
にて状態を保存するため、 Bundle
に保存できるもだけが使用できます。
サポートされてる型は ココのコード で定義されています。
また、MutableState
については特別な処理で対応していて、MutableState
で使われてる型のほうがチェックされます。
rememberSaveable に対応する
rememberSaveable
に対応するにはいくつかの方法があります。
Parcelable
一番簡単な方法は、独自クラスを Parcelable
にするだけです。
これでクラッシュせず rememberSaveable
でも使用可能になります。
Saverの実装
単純に Parcelable
に対応できない場合は Saver
を実装することで対応することもできます。
Saver
は Bundle
に保存できる型に変換して、Bundle
から復元した値を変換する処理を実装します。 実装したものをrememberSaveable
の saver
パラメータに渡すことで対応できます。
MutableState
を使う場合は stateSaver
パラメータのほうに実装した Saver
を渡します。
Android 13からOpt inにて Predictive back gesture
が使用できるようになります。これにより、バックジェスチャー(画面の左から右にスワイプするやつ)でホームに戻るときに予測できるようになります。
どのような感じは以下のドキュメントで確認できます。戻るときにホーム画面が少し見えるような感じになります。
https://developer.android.com/about/versions/13/features/predictive-back-gesture
ただし、この Predictive back gesture
によりバック関連の既存挙動が変更され、場合によってはかなりの変更が必要になります。
このドキュメントには以下のように記載されており、今後デフォルトとなり対応が必須になってくると思われます。
Caution: If you don’t update your app by the next major version of Android following 13, users will experience broken Back navigation when running your app.
Predictive back gestureのOpt in
Android 13の時点ではOpt inが必要になります。以下のように AndroidManifest.xml
に追加します。
<manifest>
<application
...
android:enableOnBackInvokedCallback="true"
...
> </application>
</manifest>
また、端末/エミュレータでは開発者オプション から有効にする必要があります。 Predictive back animations
( 日本語では 予測型「戻る」アニメーション
)を有効にする必要があります。
既存挙動の変更
Predictive back gesture が有効になると既存挙動が変更されます。onBackPressed
と KeyEvent.KEYCODE_BACK
が機能しなくなります。
この例では、 onKeyDown
を使ってますが、 onKeyUp
等でも同様です。
こういった処理は結構使用されてると思うので、影響が大きいアプリはありそうです。早めに対応を検討していったほうが良いと思います。
Predictive back gestureのサポート
Predictive back gestureのサポートをするにはPlatform APIかAndroidXを使用する必要があります。minSdk 33っていうアプリはだいぶレアかと思いますので、AndroidXを使っていくことになると思います。
今回はAndroidXの使用方法を紹介していきます。Platform APIは省略します。
androidx.activity:activity
のOnBackPressedCallback
を使うことで対応できます。これを書いてる時点では 1.6.0-alpha05
を使用しています。
Composeの場合
Composeの場合は BackHandler
を使うことで対応可能です。
注意
例えば、条件でActivityを終了させたりしなかったりするパターンで以下のような実装を思いつくと思います。
ただ、この場合に Predictive back gesture をOpt inしていたとしても、挙動が以前と同じなってしまい効果がなくなってしまいます。
なので、条件によって終了させるかどうかを実装するときは、 OnBackPressedCallback
の有効・無効を切り替えるほうが良いと思います。
無効のときは Predictive back gesture の効果が得られつつ、Activityを終了させることができます。
Composeの場合はフラグ用意して、それを切り替えて制御します。
Material 3のColor Systemには Surface tones
というものがあります。この仕様によって想定した色をうまく設定できないなど気づきにくいこともあります。この Surface tones
についてComposeの実装を踏まえて簡単にまとめておきます。
これ書いてる時点ではCompose Material 3はalphaなので今後変更の可能性あります。
Surface tones
項目の直接のリンクがないのですが、上記のドキュメントの中盤あたりに、 Surface tones
という項目があります。
Material 3におけるSurfaceにはelevationのレベルがあり、+1から+5まであります。このelevationのレベルによって色調が変化する仕様になっています。
ドキュメントのほうにコンポーネントごとのレベル例が記載されています。例えば、AppBarやBottomBarは+2という感じになっています。
androidx.compose.material3
Composeの実装である、 androidx.compose.material3 にもこの仕様が入っています。
今回は alpha14
を使っています。
androidx.compose.material3.Surface
には tonalElevation
のパラメータがあり、これを設定することでelevationレベルを設定する感じになります。デフォルは 0dp になっています。
また、この tonalElevation
は color
が MaterialTheme.colorScheme.surface
のときのみ有効になります。
以下は tonalElevation
の値による色の違いになります。値が大きいほど色が濃くなっています。surfaceの色は Color(0xFFD0BCFF)
を指定してます。