Navigation3 時代のSavedStateHandleを考え直す

Jetpack Navigation 3が登場し、画面遷移の設計は大きく変わろうとしています。

その中でも特に変化が大きいのが、ViewModelでSavedStateHandleから画面引数を取得するという、これまで当たり前だった実装です。


@HiltViewModel 
class UserViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle
) : ViewModel() { 
    val userId: String = checkNotNull(savedStateHandle["userId"]) 
}

これまで多くのサンプルや公式ドキュメントでも紹介されてきたため、この書き方に違和感を持つ人は少ないでしょう。

しかしNavigation 3では、この設計を前提としなくてもよくなりました。

むしろ、画面引数は直接ViewModelへ渡すほうが自然だという考え方へ移りつつあります。

 

🧑🏻‍💻 SavedStateHandleは本来何のためのAPIなのか

SavedStateHandleは、プロセスデス時の状態復元を目的として作られたAPIです。

Androidではメモリ不足になるとアプリのプロセスが終了されます。

その際、入力途中の内容やスクロール位置などをBundleへ保存し、アプリ復元時に元へ戻せるようにする仕組みがあります。

SavedStateHandleは、その保存領域へアクセスするためのラッパーです。

つまり、本来の役割は

- テキスト入力
- スクロール位置
- 一時的なUI状態

などを保持することであり、

画面引数を受け渡すためのAPIではありません。

 

🧑🏻‍💻 Navigation 2では便利だった

Navigation Composeでは画面引数が自動的にSavedStateHandleへ入るため、


val userId = savedStateHandle["userId"]

だけで取得できました。

非常に便利だった反面、この実装には問題もありました。

ViewModelが

- 引数名
- Navigationの仕様
- ルート構造

まで知る必要があったからです。

本来ビジネスロジックだけを持つはずのViewModelが、Navigationへ依存してしまいます。

 

🧑🏻‍💻 Navigation 3では考え方が変わった

Navigation 3では画面遷移が型安全になりました。

例えば


@Serializable
data class UserScreen(
    val userId: String
)

のようにルート自体がオブジェクトになります。

Composableでは


fun UserScreen(
    route: UserScreen
)

として受け取れるため、

ViewModelにもそのまま渡せます。


@HiltViewModel(assistedFactory = Factory::class)
class UserViewModel @AssistedInject constructor(
    @Assisted
    private val route: UserScreen
) : ViewModel()

これなら

- 文字列キーが不要
- 型安全
- Navigationへの依存が少ない

というメリットがあります。

 

🧑🏻‍💻 ViewModelはNavigationを知らなくていい

以前は


Navigation
    ↓
SavedStateHandle
    ↓
ViewModel

という流れでした。

Navigation 3では


Navigation
    ↓
Route
    ↓
ViewModel

という構成になります。

ViewModelは必要なデータだけを受け取り、Navigationの実装を意識しなくて済みます。

責務も明確になります。

 

🧑🏻‍💻 SavedStateHandleが不要になったわけではない

もちろん、SavedStateHandle自体が不要になるわけではありません。

例えば

- 検索キーワード
- フォーム入力
- スクロール位置
- タブ選択状態

など、プロセスデス後も復元したいUI状態には引き続き最適です。

つまり、

- 画面引数
- UI状態

を分けて考えることが重要になります。

 

🧑🏻‍💻 これからの設計

Navigation 2では

「画面引数=SavedStateHandle」

という書き方が一般的でした。

しかしNavigation 3では、

- Routeをそのまま渡す
- Assisted Injectionを利用する
- SavedStateHandleはUI状態だけに使う

という設計のほうがシンプルで責務も明確になります。

すぐに既存コードを書き換える必要はありませんが、新規実装ではこの考え方を取り入れていく価値は十分にあるでしょう。

Navigation 3は単なるAPIの更新ではなく、ViewModelとNavigationの関係そのものを見直すきっかけになりそうです。

 

🧑🏻‍💻 参考


Related Categories :  AndroidDevelopmemtJetpackComposeKotlinRecommended