Activity view model in Jetpack compose

Viewed 2755

In fragment, we have

private val activityViewModel: MainActivityViewModel by activityViewModels()
private val fragmentViewModel: MainFragmentViewModel by viewModels()

to get an instance of a shared view model throughout the app (activity view model) and a view specific view model (fragment view model).

I am migrating to compose.

How to get two view models with different scopes in jetpack compose?

From the docs, I can see this line,

viewModel() returns an existing ViewModel or creates a new one in the given scope.

But, how do I specify the scope of the view model?

P.S.
I have already gone through this question which is similar but it doesn't have any answers.

3 Answers

Normally within a single composite tree, such as within the setContent content, there is one view model scope that is shared between all child composites.

You can override it if you want, using LocalViewModelStoreOwner:

CompositionLocalProvider(
    LocalViewModelStoreOwner provides viewModelStoreOwner
) {
    NextComposable()
}

Compose Navigation overrides it for each navigation destination. See this answer to see how you can share it between navigation destinations.

To elaborate on @Pylyp Dukhov. Since its possible to change the default provider directly in the tree, its possible to make a function that fetch a viewModel in a specific ViewModelStore.

I've made a gist for that.

Here's the content :

/** Try to fetch a viewModel in [store] */
@Composable
inline fun <reified T : ViewModel, S : ViewModelStoreOwner> viewModelInStore(store: S): Result<T> =
    runCatching {
        var result: Result<T>? = null
        CompositionLocalProvider(LocalViewModelStoreOwner provides store) {
            result = runCatching { viewModel(T::class.java) }
        }
        result!!.getOrThrow()
    }

/** Try to fetch a viewModel with current context (i.e. activity)  */
@Composable
inline fun <reified T : ViewModel> safeActivityViewModel(): Result<T> = runCatching {
    val activity = LocalContext.current as? ViewModelStoreOwner
        ?: throw IllegalStateException("Current context is not a viewModelStoreOwner.")
    return viewModelInStore(activity)
}

/** Force fetch a viewModel inside context's viewModelStore */
@Composable
inline fun <reified T : ViewModel> activityViewModel(): T = safeActivityViewModel<T>().getOrThrow()

Getting a viewModel on activity store is as easy as getting a viewModel from the default tree's store.

@Composable
fun MyComposeElement(
    fragmentViewModel: ComposeViewModel = viewModel(),
    activityViewModel: ComposeViewModel = activityViewModel()
) {
    assert(fragmentViewModel != activityViewModel)
    assert(fragmentViewModel == viewModel<ComposeViewModel>())
    assert(activityViewModel == activityViewModel<ComposeViewModel>())
}

According to this link

For example if i have a settings navigation graph which i can navigate to theme and if i want to theme to use settingsViewModel that scope to this navigation graph i can achieve this by:

private fun NavGraphBuilder.theme(
    navController: NavController,
) {
    composable(route = MainSubScreen.Theme.route) {
        val settingsViewModel = hiltViewModel<SettingsViewModel>(
            navController.getBackStackEntry(MainSubScreen.Settings.route)
        )
        Theme(
            settingsViewModel = settingsViewModel,
            backToSettings = { navController.popBackStack() },
        )
    }
}

or if you want theme to use settingsViewModel that scope to theme itself. U can do like below or create an settingsViewModel instance directly inside your theme composable function without passing parameters around like this. If you do this you will notice that onCleared inside settingsViewModel will be called when you pressed back from theme to settings.

private fun NavGraphBuilder.theme(
    navController: NavController,
) {
    composable(route = MainSubScreen.Theme.route) {
        val settingsViewModel = hiltViewModel<SettingsViewModel>()
        Theme(
            settingsViewModel = settingsViewModel,
            backToSettings = { navController.popBackStack() },
        )
    }
}
Related