ViewModel が Compose のライフサイクルと対応してないって言ってる正確な意味はわからないけど、SingleActivity + full Compose で、Compose で作った1画面を今までの Activity や Fragment のように使いたいなら navigation-compose 使うのがいいと思う。https://t.co/fYTMoZqTLm
— Yuki Anzai (@yanzm) August 26, 2021
使用ライブラリ
以下の解説はcompose_version : 1.0.1
androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07
androidx.navigation:navigation-compose:2.4.0-alpha07
のコードをもとにしています。
内部の実装は今後変わる可能性があります。
共通コード
navigation-compose あり/なしの比較をしたいので、共通する部分のコードを先に出しておきます。- class Screen1ViewModel : ViewModel() {
- val values: List<Int> = (0 until 20).map { Random.nextInt() }
- init {
- println("Screen1ViewModel : created : $this")
- }
- override fun onCleared() {
- println("Screen1ViewModel : cleared : $this")
- }
- }
- class Screen2ViewModel : ViewModel() {
- val value = Random.nextFloat()
- init {
- println("Screen2ViewModel : created : $this")
- }
- override fun onCleared() {
- println("Screen2ViewModel : cleared : $this")
- }
- }
- @Composable
- fun Screen1(
- viewModel: Screen1ViewModel,
- onClickItem: (Int) -> Unit,
- ) {
- DisposableEffect(Unit) {
- println("Screen1 : composed : viewModel = $viewModel")
- onDispose {
- println("Screen1 : disposed")
- }
- }
- LazyColumn(modifier = Modifier.fillMaxSize()) {
- items(viewModel.values) { value ->
- Text(
- text = value.toString(),
- modifier = Modifier
- .fillMaxWidth()
- .clickable {
- onClickItem(value)
- }
- .padding(16.dp)
- )
- }
- }
- }
- @Composable
- fun Screen2(
- viewModel: Screen2ViewModel,
- value1: Int
- ) {
- DisposableEffect(Unit) {
- println("Screen2 : composed : viewModel = $viewModel")
- onDispose {
- println("Screen2 : disposed")
- }
- }
- Text(
- text = "value1 = $value1, value2 = ${viewModel.value}",
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- )
- }
- class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- MaterialTheme {
- Surface(color = MaterialTheme.colors.background) {
- MyApp()
- }
- }
- }
- }
- }
navigation-compose を使わないときの ViewModel のライフサイクル
まず navigation-compose を使わないときの動作を確認してみましょう。- @Composable
- fun MyApp() {
- val screenState = remember { mutableStateOf<Screen>(Screen.Screen1) }
- when (val screen = screenState.value) {
- Screen.Screen1 -> {
- Screen1(viewModel()) {
- screenState.value = Screen.Screen2(it)
- }
- }
- is Screen.Screen2 -> {
- BackHandler {
- screenState.value = Screen.Screen1
- }
- Screen2(viewModel(), screen.value)
- }
- }
- }
- Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@7826ac6
- Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6
- -- Screen1 の LazyColumn のアイテムをタップ
- Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@4d64aba
- Screen1 : disposed
- Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@4d64aba
- -- back キータップ
- Screen2 : disposed
- Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6
- -- Screen1 の LazyColumn のアイテムをタップ
- Screen1 : disposed
- Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@4d64aba
- -- back キータップ
- Screen2 : disposed
- Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6
- -- back キータップ(アプリ終了)
- Screen1 : disposed
- Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@7826ac6
- Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@4d64aba
viewModel() は androidx.lifecycle:lifecycle-viewmodel-compose に定義されている拡張関数で、次のようになっています。
- @Composable
- public inline fun <reified VM : ViewModel> viewModel(
- viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
- "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
- },
- key: String? = null,
- factory: ViewModelProvider.Factory? = null
- ): VM = viewModel(VM::class.java, viewModelStoreOwner, key, factory)
- @Composable
- public fun <VM : ViewModel> viewModel(
- modelClass: Class<VM>,
- viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
- "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
- },
- key: String? = null,
- factory: ViewModelProvider.Factory? = null
- ): VM = viewModelStoreOwner.get(modelClass, key, factory)
- private fun <VM : ViewModel> ViewModelStoreOwner.get(
- javaClass: Class<VM>,
- key: String? = null,
- factory: ViewModelProvider.Factory? = null
- ): VM {
- val provider = if (factory != null) {
- ViewModelProvider(this, factory)
- } else {
- ViewModelProvider(this)
- }
- return if (key != null) {
- provider.get(key, javaClass)
- } else {
- provider.get(javaClass)
- }
- }
ViewModelProvider() に ViewModelStoreOwner を渡して ViewModel のインスタンスを取得するのは、今まで Activity や Fragment でやっていたのと同じです。 Activity や Fragment は ViewModelStoreOwner を実装しています。
では Screen1 や Screen2 で viewModel() を呼ぶとき LocalViewModelStoreOwner には何がセットされているのでしょうか。
- Screen.Screen1 -> {
- println("Screen1 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")
- Screen1(viewModel()) {
- screenState.value = Screen.Screen2(it)
- }
- }
- Screen1 : ViewModelStoreOwner = com.sample.myapplication.MainActivity@7826ac6
- ...
- Screen2 : ViewModelStoreOwner = com.sample.myapplication.MainActivity@7826ac6
つまり Screen1ViewModel、Screen2ViewModel いずれも MainActivity に紐づいており、表示上は Screen1, Screen2 と遷移していますが、常に同じ MainActivity にいるので viewModel() で同じインスタンスが返ってきていたということだったのです。
navigation-compose での ViewModel のライフサイクル
navigation-compose を使った場合どうなるのか確認してみましょう。- @Composable
- fun MyApp() {
- val navController = rememberNavController()
- NavHost(navController = navController, startDestination = "Screen1") {
- composable(
- route = "Screen1"
- ) {
- println("Screen1 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")
- Screen1(viewModel()) {
- navController.navigate("Screen2/$it")
- }
- }
- composable(
- route = "Screen2/{value}",
- arguments = listOf(navArgument("value") { type = NavType.IntType })
- ) {
- val value1 = requireNotNull(it.arguments).getInt("value")
- println("Screen2 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")
- Screen2(viewModel(), value1)
- }
- }
- }
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@f79f3f3
- Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- -- Screen1 の LazyColumn のアイテムをタップ
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
- Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@7cf0642
- Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@7cf0642
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
- Screen1 : disposed
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
- -- back キータップ
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@7cf0642
- Screen2 : disposed
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- -- Screen1 の LazyColumn のアイテムをタップ
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
- Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@96e0d3b
- Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@96e0d3b
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
- Screen1 : disposed
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
- -- back キータップ
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
- Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@96e0d3b
- Screen2 : disposed
- Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
- -- back キータップ(アプリ終了)
- Screen1 : disposed
- Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@f79f3f3
ViewModelStoreOwner の部分を省くと
- Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@f79f3f3
- Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
- -- Screen1 の LazyColumn のアイテムをタップ
- Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@7cf0642
- Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@7cf0642
- Screen1 : disposed
- -- back キータップ = popBackstack
- Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
- Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@7cf0642
- Screen2 : disposed
- -- Screen1 の LazyColumn のアイテムをタップ
- Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@96e0d3b
- Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@96e0d3b
- Screen1 : disposed
- -- back キータップ = popBackstack
- Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
- Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@96e0d3b
- Screen2 : disposed
- -- back キータップ(アプリ終了)
- Screen1 : disposed
- Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@f79f3f3
LocalViewModelStoreOwner へのセット
navigation-compose では NavHost composable で LocalViewModelStoreOwner に NavBackStackEntry をセットしています。- @Composable
- public fun NavBackStackEntry.LocalOwnersProvider(
- saveableStateHolder: SaveableStateHolder,
- content: @Composable () -> Unit
- ) {
- CompositionLocalProvider(
- LocalViewModelStoreOwner provides this,
- LocalLifecycleOwner provides this,
- LocalSavedStateRegistryOwner provides this
- ) {
- saveableStateHolder.SaveableStateProvider(content)
- }
- }
- @Composable
- public fun NavHost(
- navController: NavHostController,
- graph: NavGraph,
- modifier: Modifier = Modifier
- ) {
- ...
- if (backStackEntry != null) {
- ...
- Crossfade(backStackEntry.id, modifier) {
- val lastEntry = transitionsInProgress.lastOrNull { entry ->
- it == entry.id
- } ?: backStack.lastOrNull { entry ->
- it == entry.id
- }
- lastEntry?.LocalOwnersProvider(saveableStateHolder) {
- (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
- }
- ...
- }
- }
- ...
- }
NavBackStackEntry の ViewModelStoreOwner 実装
では NavBackStackEntry が ViewModelStoreOwner をどのように実装しているのか見てみましょう。- public class NavBackStackEntry private constructor(
- ...
- private val viewModelStoreProvider: NavViewModelStoreProvider? = null,
- ...
- ) : LifecycleOwner,
- ViewModelStoreOwner,
- HasDefaultViewModelProviderFactory,
- SavedStateRegistryOwner {
- ...
- public override fun getViewModelStore(): ViewModelStore {
- ...
- checkNotNull(viewModelStoreProvider) {
- ...
- }
- return viewModelStoreProvider.getViewModelStore(id)
- }
- }
NavViewModelStoreProvider は interface で、backStackEntryId: String から ViewModelStore を返すメソッドが定義されています。
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public interface NavViewModelStoreProvider {
- public fun getViewModelStore(backStackEntryId: String): ViewModelStore
- }
- internal class NavControllerViewModel : ViewModel(), NavViewModelStoreProvider {
- private val viewModelStores = mutableMapOf<String, ViewModelStore>()
- fun clear(backStackEntryId: String) {
- // Clear and remove the NavGraph's ViewModelStore
- val viewModelStore = viewModelStores.remove(backStackEntryId)
- viewModelStore?.clear()
- }
- override fun onCleared() {
- for (store in viewModelStores.values) {
- store.clear()
- }
- viewModelStores.clear()
- }
- override fun getViewModelStore(backStackEntryId: String): ViewModelStore {
- var viewModelStore = viewModelStores[backStackEntryId]
- if (viewModelStore == null) {
- viewModelStore = ViewModelStore()
- viewModelStores[backStackEntryId] = viewModelStore
- }
- return viewModelStore
- }
- ...
- companion object {
- private val FACTORY: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
- @Suppress("UNCHECKED_CAST")
- override fun <T : ViewModel> create(modelClass: Class<T>): T {
- return NavControllerViewModel() as T
- }
- }
- @JvmStatic
- fun getInstance(viewModelStore: ViewModelStore): NavControllerViewModel {
- val viewModelProvider = ViewModelProvider(viewModelStore, FACTORY)
- return viewModelProvider.get()
- }
- }
- }
NavControllerViewModel は NavHost composable が呼び出される時点での LocalViewModelStoreOwner にセットされている ViewModelStoreOwner を使って生成され、NavController にセットされます。
- @Composable
- public fun NavHost(
- navController: NavHostController,
- graph: NavGraph,
- modifier: Modifier = Modifier
- ) {
- val lifecycleOwner = LocalLifecycleOwner.current
- val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
- "NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
- }
- ...
- navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
- ...
- }
- public open class NavHostController(context: Context) : NavController(context) {
- ...
- public final override fun setViewModelStore(viewModelStore: ViewModelStore) {
- super.setViewModelStore(viewModelStore)
- }
- }
- public open class NavController(
- ...
- ) {
- ...
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public open fun setViewModelStore(viewModelStore: ViewModelStore) {
- if (viewModel == NavControllerViewModel.getInstance(viewModelStore)) {
- return
- }
- check(backQueue.isEmpty()) { "ViewModelStore should be set before setGraph call" }
- viewModel = NavControllerViewModel.getInstance(viewModelStore)
- }
- ...
- }
つまり NavBackStackEntry を ViewModelStoreOwner として ViewModel を取得する場合、NavControllerViewModel に ViewModelStore が作られ、対応する backStackEntryId との Map で保持されます。
NavController では NavBackStackEntry が backstack から pop されたときに、NavControllerViewModel で保持されている対応する ViewModelStore をクリアしています。