Activity view model in Jetpack compose
Asked Answered
P

3

8

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.

Psychogenic answered 29/8, 2021 at 8:38 Comment(0)
C
7

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.

Conceptionconceptual answered 29/8, 2021 at 8:47 Comment(2)
I can create an instance of a viewModel in Activity's onCreate and pass it to each route. What's the difference?Humanly
@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 codeConceptionconceptual
I
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>())
}
Inquisitionist answered 1/6, 2022 at 8:29 Comment(1)
It's not work in Dialog, throw Exception "Current context is not a viewModelStoreOwner."Flatting
T
1
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://mcmap.net/q/301625/-how-to-get-activity-in-compose

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

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

        context = context.baseContext
    }

    return null
}
Twelvemo answered 12/5, 2023 at 11:58 Comment(1)
This is simple and worked for me only the get activity did some minor refactoring fun Context.getActivity(): ComponentActivity? = when (this) { is ComponentActivity -> this is ContextWrapper -> baseContext.getActivity() else -> null }Rumormonger

© 2022 - 2024 — McMap. All rights reserved.