17

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?

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
jpm
  • 3,300
  • 1
  • 19
  • 29

1 Answers1

25

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>()
        }
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • 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! :) – jpm Aug 31 '21 at 16:40
  • 2
    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: https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal and https://developer.android.com/jetpack/compose/compositionlocal – jpm Sep 01 '21 at 14:49
  • 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...? – jpm Sep 01 '21 at 14:55
  • 1
    @jpm it's probably about splitting independent screen scopes, not sure really. I suggest you [creating an issue](https://issuetracker.google.com/issues/new?component=612128&template=1253476) on compose issue tracker to suggest option to disable this behavior. – Phil Dukhov Sep 01 '21 at 17:20
  • Is it better to pass the `viewModelStoreOwner` or the `ViewModel` itself to child composables? – AndroidKotlinNoob Apr 29 '22 at 14:35
  • 1
    @AndroidKotlinNoob It's depends on the situation. I added more details to my answer, check it out – Phil Dukhov Apr 29 '22 at 14:54
  • Note, though, that `CompositionLocal` should *not* be used to pass around a `ViewModel`: https://developer.android.com/jetpack/compose/compositionlocal#alternatives – Björn Oct 05 '22 at 10:47