Jetpack Lifecycle 2.11.0 で rememberViewModelStoreProvider が追加され、特定の Composable fun のスコープに ViewModel を紐づけることができるようになりました。
これを書いているのは Lifecycle 2.11.0-beta01 時点の内容なので、使用する際はライブラリのバージョンと最新情報を併せて確認してください。
実装
rememberViewModelStoreProvider と rememberViewModelStoreOwner を使用して、そのスコープ内だけの ViewModelStoreOwner を作ります。rememberViewModelStoreProvider の呼び出しスコープで ViewModel の生存範囲が決まります。
LocalViewModelStoreOwner に渡すことで、スコープ内での ViewModel の生存期間が rememberViewModelStoreProvider の呼び出したスコープになります。
val storeProvider = rememberViewModelStoreProvider()
val owner = rememberViewModelStoreOwner(provider = storeProvider)
CompositionLocalProvider(LocalViewModelStoreOwner provides owner) {
val viewModel = viewModel { HogeViewModel() }
}
ユースケース
Pager
HorizontalPager/VerticalPager と組み合わせることで、Composition 中の Pager のコンテンツのスコープで ViewModel を扱うことができます。
rememberViewModelStoreProvider の呼び出しのスコープで ViewModel の生存期間が決まるため、Pager の外側に置くことで Pager のスコープの範囲で ViewModel のインスタンスが保持されます。viewModel(key = XXX) でインスタンスを Pager のコンテンツごとに分けていたこれまでのよくある実装と同様の処理になります。
@Composable
fun PagerSample() {
val storeProvider = rememberViewModelStoreProvider()
val pages = listOf(...)
HorizontalPager(pageCount = pages.size) { page ->
val pageOwner = rememberViewModelStoreOwner(provider = storeProvider, key = page)
CompositionLocalProvider(LocalViewModelStoreOwner provides pageOwner) {
val pageViewModel = viewModel { HogeViewModel(pages[page]) }
}
}
}
Dialog
Dialog 系と組み合わせることで、Dialog が表示しているスコープで ViewModel を扱うことができます。
Scoped ViewModel を使わない実装の場合、Dialog を再度表示した時に以前表示した ViewModel のインスタンスがそのままなので ViewModel の状態が使いまわされてしまう、ということがありましたが、Scoped ViewModel を使うことで表示ごとに異なるインスタンスが使われるので回避することができます。
@Composable
fun PagerSample() {
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
if (openBottomSheet) {
val scopedOwner = rememberViewModelStoreOwner()
CompositionLocalProvider(LocalViewModelStoreOwner provides scopedOwner) {
val viewModel = viewModel { HogeViewModel() }
ModalBottomSheet(
...
) {
...
}
}
}
}