7

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.

Abhimanyu
  • 11,351
  • 7
  • 51
  • 121

4 Answers4

6

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.

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • I can create an instance of a viewModel in Activity's `onCreate` and pass it to each route. What's the difference? – Jim Ovejera Mar 18 '22 at 08:44
  • 1
    @JimOvejera `viewModel()` will create/cache a view model for you, passing it down from an activity is an option, but it requires a lot of boilerplate code – Phil Dukhov Mar 18 '22 at 09:33
5

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>())
}
Lionel Briand
  • 1,732
  • 2
  • 13
  • 21
0
class MainViewModel: ViewModel() { }

@Composable
fun MainScreen() {
    // ViewModel with a scope of the current class.
    val screenViewModel = viewModel<MainViewModel>()
    
    // ViewModel with a scope of the activity.
    val activity = LocalContext.current.getActivity()
    activity?.let {
        val activityViewModel = viewModel<MainViewModel>(viewModelStoreOwner = activity)
    }
}

Please make sure to import compose viewModel.

import androidx.lifecycle.viewmodel.compose.viewModel

In addition, getActivity() function is not directly available, please use the following Context extension. Reference: https://stackoverflow.com/a/67927037/6512100

fun Context.getActivity(): AppCompatActivity? {
    var context = this

    while (context is ContextWrapper) {
        if (context is AppCompatActivity) return context

        context = context.baseContext
    }

    return null
}
-1

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() },
        )
    }
}
adwardwo1f
  • 817
  • 6
  • 18