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!
In android, we can use ViewModel to share data between various fragments or activities by sharing the same ViewModel among all the fragments and they can access everything defined in the ViewModel. This is one way to have communication between fragments or activities.
There are two ways to declare the data within a ViewModel so that it is observable. One option is to use the Compose state mechanism which has been used extensively throughout this book. An alternative approach is to use the Jetpack LiveData component, a topic that will be covered later in this chapter.
1 Answer. Show activity on this post. Compose can recompose when some with mutable state value container changes. You can create it manually with mutableStateOf() , mutableStateListOf() , etc, or by wrapping Flow / LiveData .
If you use the Architecture Components ViewModel library, you can access a ViewModel from any composable by calling the viewModel() function.
The ViewModel NavGraph integration was one of the new navigation features announced at I/O 2019. For more, check out the talk Jetpack Navigation and the documentation. This integration is an oldie but a goodie. ViewModels usually contain LiveData, and LiveData is meant to be observed. Usually this means adding an observer in fragment:
To navigate to a composable destination in the navigation graph, you must use the navigate () method. navigate () takes a single String parameter that represents the destination’s route. To navigate from a composable within the navigation graph, call navigate ():
To support Compose, use the following dependency in your app module’s build.gradle file: The NavController is the central API for the Navigation component. It is stateful and keeps track of the back stack of composables that make up the screens in your app and the state of each screen.
Since its introduction, ViewModel has become one of the most “core” Android Jetpack libraries. Based on our 2019 Developer Benchmarking data, over 40% of Android Developers have added ViewModels to their apps.
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.
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")
}
}
}
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)
...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With