'Sharing viewModel within Jetpack Compose Navigation
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!
Solution 1:[1]
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")
}
}
}
Solution 2:[2]
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.
Solution 3:[3]
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)
...
}
Solution 4:[4]
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()
) {
// ...
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Geert Berkers |
Solution 2 | |
Solution 3 | ch4k4uw |
Solution 4 | msickpaler |