🫂

[Jetpack Compose] 複数画面でViewModelを共有する

2024/05/03に公開

前提・環境

  • Jetpack Compsose
  • Navigation Compose

結論

複数画面にまたがるフローの状態を保持するViewModelを使いたいなら、viewModelStoreOwner をうまく使おう。

navigation("login") {
  composable("input-email") { backStackEntry ->
    val parentEntry = remember(backStackEntry) {
      navController.getBackStackEntry("parentNavigationRoute")
    }
    val parentViewModel: SharedViewModel = viewModel(viewModelStoreOwner = parentEntry)
  }
}

参考: https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-apis#vm-api-navgraph

複数画面にまたがるViewModelが欲しくなる場面とは

例えばアプリのログインフローを実装するにあたって以下のようなナビゲーションが設定されていたとします。

ログインのナビゲーション
navigation("login", startDestination = "input-email") {
  composable("input-email") {
    // メアドを入力する画面
    InputEmailScreen()
  }
  composable("input-password") {
    // パスワードを入力し、ログインを試みる画面
    InputPasswordScreen()
  }
}


画面イメージ

この時、input-email で入力したEmailは"input-password"のログインボタンをクリックしたときに使用する必要があるため保持しておいていかなかればいけません

このように複数画面にまたがったフローで状態を保持したいユースケースがあります。

他にも商品の購入のフローや複雑なデータの登録フローなどもこれにあたるでしょう。

公式ドキュメントを参照する

https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-apis#vm-api-navgraph

複数画面で状態を保持するViewModelのことをNavigationライブラリの公式ドキュメントでは ViewModels scoped to the Navigation graph (Navigation graphにスコープされたViewModel) と呼んでいます。

ドキュメントをもとにわかりやすくサンプルを示します。

navigation("login", startDestination = "input-email") {
  composable("input-email") {
    val parentEntry = remember(backStackEntry) {
      navController.getBackStackEntry("login")
    }
    val loginViewModel: LoginViewModel = viewModel(viewModelStoreOwner = parentEntry)  // この LoginViewModel は "input-password" と同じインスタンスを参照す

    // メアドを入力する画面
    InputEmailScreen(loginViewModel = loginViewModel)
  }
  composable("input-password") {
    val parentEntry = remember(backStackEntry) {
      navController.getBackStackEntry("login")
    }
    val loginViewModel: LoginViewModel = viewModel(viewModelStoreOwner = parentEntry)  // この LoginViewModel は "input-email" と同じインスタンスを参照する

    // パスワードを入力し、ログインを試みる画面
    InputPasswordScreen(loginViewModel = loginViewModel)
  }
}

この例では viewModel() 関数のviewModelStoreOwnerに親の BackStackEntry を渡しています。

viewModelの生存期間は通常その画面に一番近いBackStackEntryと同じになります。(実際viewModel関数のviewModelStoreOwner引数のデフォルト値は LocalViewModelStoreOwner.current を参照しています)。
しかし、明示的に viewModelStoreOwner を指定することで、ViewModelをどのBackStackEntry(などのViewModelStoreOwner) に結びつけるかを指定することができるようです。

Discussion