Why a new ViewModel is created in each Compose Navigation route?
Asked Answered
D

2

23

I have a single activity app using only composables for the ui (one activity, no fragments). I use one viewmodel to keep data for the ui in two different screens (composables). I create the viewmodel in both screens as described in state documentation

@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) 

Now I noticed that the data that was loaded or set in the first screen is reset in the second.

I also noticed that init{} is called every time viewModel() is called. Is this really the expected behavior?

According to the method's documentation it should return either an existing ViewModel or create a new one.

I also see that the view models are different objects. So viewModel() always creates a new one. But why?

Any ideas what I could be doing wrong? Or do I misunderstand the usage of the method?

Drinker answered 31/8, 2021 at 16:8 Comment(0)
D
36

Usually view model is shared for the whole composables scope, and init shouldn't be called more than once.

But if you're using compose navigation, it creates a new model store owner for each destination. If you need to share models between destination, you can do it like in two ways:

  1. By passing it directly to viewModel call. In this case only the passed view model will be bind to parent store owner, and all other view models created inside will be bind(and so destroyed when route is removed from the stack) to current route.
  2. By proving value for LocalViewModelStoreOwner, so all composables inside will be bind to the parent view model store owner, and so are not gonna be freed when route is removed from the stack.
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
    "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
    composable("first") {
        val model = viewModel<Model>(viewModelStoreOwner = viewModelStoreOwner)
    }
    composable("second") {
        CompositionLocalProvider(
            LocalViewModelStoreOwner provides viewModelStoreOwner
        ) {
            val model = viewModel<Model>()
        }
    }
}
Diaper answered 31/8, 2021 at 16:26 Comment(7)
Hi Philip! Thank you. I do use navigation, so this sounds promisin. So far I assumed, that I didn't have to consider this, since I only had the one activity. I'll try it! :)Drinker
Hej Philip thank you for introducing me to CompositionLocal! I used the second approach, I had some difficulties, but it works now! If somebody else stumbles upon this, this helped me: developer.android.com/reference/kotlin/androidx/compose/runtime/… and developer.android.com/jetpack/compose/compositionlocalDrinker
Are there any reasons not to use it for every composable in the navigation graph? It seems obvious to me that we would want the same ViewModelStoreOwner for every route...?Drinker
@Drinker it's probably about splitting independent screen scopes, not sure really. I suggest you creating an issue on compose issue tracker to suggest option to disable this behavior.Diaper
Is it better to pass the viewModelStoreOwner or the ViewModel itself to child composables?Nada
@Nada It's depends on the situation. I added more details to my answer, check it outDiaper
Note, though, that CompositionLocal should not be used to pass around a ViewModel: developer.android.com/jetpack/compose/…Calipash
M
0

I have created a sample where I bound ViewModelStoreOwner to a composable lifecycle and it seems to work pretty well. https://gist.github.com/OKatrych/7d7dc5f0c462a25bfd3a9d34026ba0ba

Mancino answered 20/9, 2023 at 19:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.