63

Can anyone suggest how to share a ViewModel within different sections of a Jetpack Compose Navigation?

According to the documentation, viewModels should normally be shared within different compose functions using the activity scope, but not if inside the navigation.

Here is the code I am trying to fix. It looks like I am getting two different viewModels here in two sections inside the navigation:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            NavigationSystem()
        }
    }
}

@Composable
fun NavigationSystem() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") { HomeScreen(navController) }
        composable("result") { ResultScreen(navController) }
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    val viewModel: ConversionViewModel = viewModel()
    
    var temp by remember { mutableStateOf("") }
    val fahrenheit = temp.toIntOrNull() ?: 0

    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Column {
            OutlinedTextField(
                value = temp,
                onValueChange = { temp = it },
                label = { Text("Fahrenheit") },
                modifier = Modifier.fillMaxWidth(0.85f)
            )

            Spacer(modifier = Modifier.padding(top = 16.dp))

            Button(onClick = {
                Log.d("HomeScreen", fahrenheit.toString())
                if (fahrenheit !in 1..160) return@Button
                viewModel.onCalculate(fahrenheit)
                navController.navigate("result")
            }) {
                Text("Calculate")
            }
        }
    }
}

@Composable
fun ResultScreen(navController: NavController) {
    val viewModel: ConversionViewModel = viewModel()

    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Log.d("ResultScreenDebug", "celsius: ${ viewModel.celsius.value.toString()}")
        Text(
            viewModel.celsius.value.toString(),
            style = MaterialTheme.typography.h6
        )

        Spacer(modifier = Modifier.padding(top = 24.dp))

        Button(onClick = { navController.navigate("home") }) {
            Text(text = "Calculate again")
        }
    }
}

Debug log:

2021-07-27 22:01:52.542 27113-27113/com.example.navigation D/ViewModelDebug: fh: 65, cs: 18, celcius: 18.0
2021-07-27 22:01:52.569 27113-27113/com.example.navigation D/ResultScreenDebug: celsius: 0.0

Thanks!

1

8 Answers 8

31

Consider passing your activity to viewModel() fun as viewModelStoreOwner parameter since ComponentActivity implements ViewModelStoreOwner interface:

val viewModel: ConversionViewModel = viewModel(LocalContext.current as ComponentActivity)

This code will return the same instance of ConversionViewModel in all your destinations.

Sign up to request clarification or add additional context in comments.

5 Comments

Passing acitivity to the view model is a bad pattern. These two should always be separated.
@ShadeToD I'm not passing activity instance to the ViewModel constructor, I'm passing it to viewModel() fun as an instance of ViewModelStoreOwner.
If you rotated the phone data would viewmodel data be retained?
Thanks a lot this really helped, it works the same as val viewModel by activityViewModels<ConversionViewModel>() to use one instance between fragments.
Heartfelt Thanks! It worked.
23

You could create a viewModel and pass it trough

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            NavigationSystem()
        }
    }
}

@Composable
fun NavigationSystem() {
    val navController = rememberNavController()

    val viewModel: ConversionViewModel = viewModel()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") { HomeScreen(navController, viewModel) }
        composable("result") { ResultScreen(navController, viewModel) }
    }
}

@Composable
fun HomeScreen(navController: NavController, viewModel: ConversionViewModel) {
    var temp by remember { mutableStateOf("") }
    val fahrenheit = temp.toIntOrNull() ?: 0

    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Column {
            OutlinedTextField(
                value = temp,
                onValueChange = { temp = it },
                label = { Text("Fahrenheit") },
                modifier = Modifier.fillMaxWidth(0.85f)
            )

            Spacer(modifier = Modifier.padding(top = 16.dp))

            Button(onClick = {
                Log.d("HomeScreen", fahrenheit.toString())
                if (fahrenheit !in 1..160) return@Button
                viewModel.onCalculate(fahrenheit)
                navController.navigate("result")
            }) {
                Text("Calculate")
            }
        }
    }
}

@Composable
fun ResultScreen(navController: NavController, viewModel: ConversionViewModel) {
    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Log.d("ResultScreenDebug", "celsius: ${ viewModel.celsius.value.toString()}")
        Text(
            viewModel.celsius.value.toString(),
            style = MaterialTheme.typography.h6
        )

        Spacer(modifier = Modifier.padding(top = 24.dp))

        Button(onClick = { navController.navigate("home") }) {
            Text(text = "Calculate again")
        }
    }
}

6 Comments

It worked perfectly, thanks a million! I was asking this question everywhere, but no one seems to know the answer yet. I hope this thread will help many others in the future.
Also a side question: I tried to initialize the view model in onCreate() of the activity and pass it to the main compose function, but it shows an error if I try to initialize it there like this: val viewModel: ConversionViewModel = viewModel(). Can you please explain why?
You have to put the function in setContent { } because =viewmodel is a Composable function, so it needs to be called from a composable scope.
While this solution works, it seems like the official documentation says that this approach is a no-go: 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.
@BenjyTec "should never pass down ViewModel instances to other composables, pass only the data they need and functions that perform the required logic as parameters" means: do not pass the entire viewModel from the screen composable to the lower composable(for example it only needs to display the userName stored in the viewModel, the rest of the information in the viewmodel is not necessary for this composable). Since we are currently only sharing the viewModel between two screen composable , viewmdoel is originally all the states that Scrren composable depends on, there is no problem.
|
17

I think a better solution, than scopes your ViewModel to your entire NavGraph is to build the ViewModel in the Home route and then access from the Result route (route scoped):

//extensions
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry?.viewModel(): T? = this?.let {
    viewModel(viewModelStoreOwner = it)
}

@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.viewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    }
): T {
    return androidx.lifecycle.viewmodel.compose.viewModel(
        viewModelStoreOwner = viewModelStoreOwner, key = T::class.java.name
    )
}

//use-case
@Composable
fun HomeScreen(navController: NavController) {
    val viewModel: ConversionViewModel = viewModel()
    ...
}
@Composable
fun ResultScreen(navController: NavController) {
    val viewModel: ConversionViewModel? = navController.previousBackStackEntry.viewModel()
    ...
}

But if you must to scope it to the entire NavGraph, you can do something like the @akhris said, but in a way that you could uncouple the ViewModelStoreOwner from the Activity:

//composable store-owner builder
@Composable
fun rememberViewModelStoreOwner(): ViewModelStoreOwner {
    val context = LocalContext.current
    return remember(context) { context as ViewModelStoreOwner }
}

This way you uncouple the Activity from your ViewModelStoreOwner and can do something like:

val LocalNavGraphViewModelStoreOwner =
    staticCompositionLocalOf<ViewModelStoreOwner> {
        TODO("Undefined")
    }

@Composable
fun NavigationSystem() {
    val navController = rememberNavController()
    val vmStoreOwner = rememberViewModelStoreOwner()

    CompositionLocalProvider(
        LocalNavGraphViewModelStoreOwner provides vmStoreOwner
    ) {
        NavHost(navController = navController, startDestination = "home") {
            composable("home") { HomeScreen(navController) }
            composable("result") { ResultScreen(navController) }
        }
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    val viewModel: ConversionViewModel = viewModel(viewModelStoreOwner = LocalNavGraphViewModelStoreOwner.current)
    ...
}

@Composable
fun ResultScreen(navController: NavController) {
    val viewModel: ConversionViewModel = viewModel(viewModelStoreOwner = LocalNavGraphViewModelStoreOwner.current)
    ...
}

2 Comments

Cool this should be an official implementation I hope, thanks @YehezkielL
This looks clean. But lets say I'm on my HomeScreen, navigate to the SecondScreen, and what ever selected on SecondScreen should be accessible from the HomeScreen in the viewModel.
13

A recommended approach, if you want to access the ViewModel scoped to navigation routes or the navigation graph i.e. that is shared between navigation routes or the navigation graph, you should use:

@Composable 
fun MyApp() {
    val navController = rememberNavController()
    val startRoute = "example"
    val innerStartRoute = "exampleWithRoute"
    NavHost(navController, startDestination = startRoute) {
        navigation(startDestination = innerStartRoute, route = "Parent") {
            composable("exampleWithRoute") { backStackEntry ->
                //IMPORTANT PART: getting the scoped ViewModel reference.

                val parentEntry = remember(backStackEntry) {
                    navController.getBackStackEntry("Parent")
                }
                val parentViewModel = hiltViewModel<ParentViewModel>(parentEntry)

                ExampleWithRouteScreen(parentViewModel)
            }
        }
    }
}

To make it easier, you may use the following extension functions might be useful:

@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 you can simply call inside your composable route:

val parentViewModel = backStackEntry.sharedViewModel<ParentViewModel>(navController)

You may also want to watch this Philipp Lackner's video on the topic.

Hope it helps!

2 Comments

What are the implications of returning hiltViewModel() if the parent's route is null? e.g val navGraphRoute = destination.parent?.route ?: return hiltViewModel()
how to use this extension function using koin? Also if ViewModel takes usecases as params, how to use that also?
2

Imagine you have two Screen A and B which both want to use a shared viewmodel using hiltViewModel. Your implementation should be like this:

@Composable
fun ScreenA(
    navigateToScreenB: () -> Unit,
    viewModel: AViewModel
) {
    // ...
}

@HiltViewModel
class AViewModel @Inject constructor(
    // ...
) : ViewModel() {
    // ...
}

@Composable
fun ScreenB(
    viewModel: AViewModel
) {
    // ...
}

// Main Activity describing our composables:
composable("ScreenA") { backStackEntry ->
    val viewModel: AViewModel = hiltViewModel(backStackEntry)
    ScreenA(
        viewModel = viewModel,
        navigateToScreenB = {
            navController.navigate("ScreenB")
        }
    )
}

composable("ScreenB") { backStackEntry ->
    val viewModel: AViewModel =
        if (navController.previousBackStackEntry != null) hiltViewModel(
            navController.previousBackStackEntry!!
        ) else hiltViewModel()
    ScreenB(
        viewModel = viewModel
    )
}

Helpful doc and article: https://developer.android.com/develop/ui/compose/libraries#hilt https://medium.com/@mousaieparniyan/shared-viewmodel-in-jetpack-compose-1328f5c895c5

Comments

0

Using the @Geraldo Neto approach you can use it in Koin DI and within each navigation graph. It will be shared between navigation routes / composables within the scope of that graph.

fun NavGraphBuilder.authGraph(
    navController: NavHostController
) {

    navigation(
        startDestination = "login_screen",
        route = "auth"
    ) {

        composable("login_screen") {
            val viewModel = it.koinSharedViewModel<AuthViewModel>(navController = navController)

            LoginScreen(navController, viewModel)
        }
    }
}

Extension function used above to create shared ViewModel using Koin

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

    return getViewModel<T>(owner = parentEntry)
}

Hope it helps!

Comments

0

In type safe jetpackNavigation if your using hilt you can init view model using route:

hiltViewModel<MyViewModel>(MyRoute)

Comments

-1

Is this not good enough?

Get the sharedViewModel at the top MainScreen and pass it explicitly. It doesn't seem to cause any memory leaks.

// MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainScreen()
        }
    }
}

@Composable
fun MainScreen(sharedViewModel: SharedViewModel = viewModel()) {
    val navController = rememberNavController()
    Navigation(navController, sharedViewModel)
}
@Composable
// Navigation.kt
fun Navigation(navController: NavHostController, sharedViewModel: SharedViewModel) {
    NavHost(navController, startDestination = "Home") {
        composable("Home") {
            EpisodeListScreen(navController, sharedViewModel)
        }
        composable("Login") {
            LoginScreen(navController, sharedViewModel)
        }
        composable("Editor") {
            EditorScreen(navController, sharedViewModel)
        }
        composable("Setting") {
            SettingScreen(navController, sharedViewModel)
        }
    }
}
// EpisodeListScreen.kt
@Composable
fun EpisodeListScreen(
    navController: NavController,
    sharedViewModel: SharedViewModel = viewModel()
) {
    // ...
}

1 Comment

in my opinion this is not clean way at all

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.