android-jetpack-composeandroid-viewmodelandroid-jetpack-navigation

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

  • 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")
            }
        }
    }