How to share a viewmodel between two or more Jetpack composables inside a Compose NavGraph?
Asked Answered
R

4

26

Consider this example.

For authentication, we'll be using 2 screens - one screen to enter phone number and the other to enter OTP.

Both these screens were made in Jetpack Compose and the for the NavGraph, we are using compose navigation.

Also I have to mention that DI is being handled by Koin.

val navController = rememberNavController()

NavHost(navController) {
    navigation(
        startDestination = "phone_number_screen",
        route = "auth"
    ) {
        composable(route = "phone_number_screen") {
            // Get's a new instance of AuthViewModel
            PhoneNumberScreen(viewModel = getViewModel<AuthViewModel>())
        }

        composable(route = "otp_screen") {
            // Get's a new instance of AuthViewModel
            OTPScreen(viewModel = getViewModel<AuthViewModel>())
        }
    }
}

So how can we share the same viewmodel among two or more composables in a Jetpack compose NavGraph?

Rosco answered 20/8, 2021 at 6:29 Comment(0)
G
29

You can to pass your top viewModelStoreOwner to each destination

  1. directly passing to .viewModel() call, composable("first") in my example
  2. overriding LocalViewModelStoreOwner for the whole content, so each composable inside CompositionLocalProvider will have access to the same view models, composable("second") in my example
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
    "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
    composable("first") {
        val model = viewModel<SharedModel>(viewModelStoreOwner = viewModelStoreOwner)
    }
    composable("second") {
        CompositionLocalProvider(
            LocalViewModelStoreOwner provides viewModelStoreOwner
        ) {
            SecondScreen()
        }
    }
}

In the second case, you can get your model at any level of the composition tree, which is inside the CompositionLocalProvider:

@Composable
fun SecondScreen() {
    val model = viewModel<SharedModel>()
    SomeView()
}

@Composable
fun SomeView() {
    val model = viewModel<SharedModel>()
}
Genotype answered 20/8, 2021 at 6:35 Comment(2)
Does SaveStateHandle do the same job? So we can use the ViewModel in the last saved state in another screen ??Usia
@Usia did you mean SavedStateHandle? it only contains navigation item bundle, it doesn't control the current screen. Not sure how it can do the same jobGenotype
L
14

Using Hilt you could do something like the below. But since you are using Koin I don't know the way of Koin yet.

@Composable
fun MyApp() {
    NavHost(navController, startDestination = startRoute) {
        navigation(startDestination = innerStartRoute, route = "Parent") {
            // ...
            composable("exampleWithRoute") { backStackEntry ->
                val parentEntry = remember {
                  navController.getBackStackEntry("Parent")
                }
                val parentViewModel = hiltViewModel<ParentViewModel>(
                  parentEntry
                )
                ExampleWithRouteScreen(parentViewModel)
            }
        }
    }
}

Official doc: https://developer.android.com/jetpack/compose/libraries#hilt

Lesko answered 26/8, 2021 at 18:41 Comment(2)
This is the recommended method by Google and it works perfectly. Just take this into consideration "Any ViewModel objects created in this way live until the associated NavHost and its ViewModelStore are cleared or until the navigation graph is popped from the back stack."Armet
@Rafiul, how can apply for this one stackoverflow.com/q/71942247/13614484?Saucer
B
2

Its simplest and efficient way to handle this

@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(
    navController: NavController
): T {
    val navGraphRoute = destination.parent?.route ?: return ViewModel()
    val parentEntry = remember(this) {
        navController.getBackStackEntry(navGraphRoute)
    }
    return ViewModel(parentEntry)
}

Then call it from composefun

composable("route") {
            it.sharedViewModel<ThoughtViewModel>(navController = navController).apply {
               RequestsScreen(this@apply)
            }
           
        }
Bergen answered 13/9, 2023 at 11:30 Comment(0)
U
0

Here is an other way with Koin.

It strictly do the same than the validated answer but simpler to write. It will have exactly the same viewModelStoreOwner without having to write it explicitly. Please tell me if i'm wrong.

val navController = rememberNavController()

val sharedViewModel = getViewModel()

NavHost(navController = navController, startDestination = "first") {
    composable("first") {
        // You can use sharedViewModel
    }
    composable("second") {
        // You can use sharedViewModel
    }
}
Unlatch answered 12/10, 2022 at 18:31 Comment(1)
I think the difference to the accepted answer is that with Phil Dukhov's answer, you can get the same viewmodel instance directly within every composable. With your approach, you need to pass the viewmodel instance into the composable as a parameter. The latter is not recommended from the official documentation: You should never pass down ViewModel instances to other composables, pass only the data they need and functions that perform the required logic as parameters.Pains

© 2022 - 2024 — McMap. All rights reserved.