I am trying to understand how to use a ViewModel that holds values that can be passed to different screens.
I've written a very simple program to try to understand this concept.
Here's my MainActivity:
sealed class Destination(val route: String) {
object PlayerSelection: Destination("PlayerSelection")
object TwoPlayerGame: Destination("TwoPlayerGame")
object ThreePlayerGame: Destination("ThreePlayerGame")
object FourPlayerGame: Destination("FourPlayerGame")
}
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Viewmodels_and_NavigationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ScreenSetup(viewModel)
}
}
}
}
}
Here's my NavigationAppHost :
@Composable
fun NavigationAppHost (navController: NavController, viewModel: MainViewModel) {
NavHost(navController = navController as NavHostController, startDestination = "PlayerSelection") {
composable(Destination.PlayerSelection.route) { PlayerSelection(navController,MainViewModel()) }
composable(Destination.TwoPlayerGame.route) {TwoPlayerGame(navController,MainViewModel())}
composable(Destination.ThreePlayerGame.route) {ThreePlayerGame(navController,MainViewModel())}
composable(Destination.FourPlayerGame.route) {FourPlayerGame(navController,MainViewModel())}
}
}
Here's my ScreenSetup :
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScreenSetup(viewModel: MainViewModel) {
//Navigation Stuff
val navController = rememberNavController()
Scaffold(
topBar = { TopAppBar(title = { Text(text = "Viewmodels and Navigation") }) },
content = { padding ->
Column(Modifier.padding(padding)) {
NavigationAppHost(navController = navController,viewModel)
}
},
bottomBar = { Text(text = "BottomBar") }
)
}
Here's the Composable PlayerSelection :
@Composable
fun PlayerSelection(navController: NavController, viewModel: MainViewModel) {
val HowManyPlayers: Int by viewModel.howManyPlayers.collectAsState()
Column {
Row {
Text(text = "How Many Players?")
}
Row() {
Button(onClick = { viewModel.Selection(2) }) {
Text("Two Players")
}
Button(onClick = { viewModel.Selection(3) }) {
Text("Three Players")
}
Button(onClick = { viewModel.Selection(4) }) {
Text("Four Players")
}
}
Row() {
Text(text = "Current Value Held in MainViewModel = ${HowManyPlayers}")
}
Button(onClick = {
if (HowManyPlayers == 2) {
navController.navigate(Destination.TwoPlayerGame.route)
}
if (HowManyPlayers == 3) {
navController.navigate(Destination.ThreePlayerGame.route)
}
if (HowManyPlayers == 4) {
navController.navigate(Destination.FourPlayerGame.route)
}
}) {
Text(text = "Click To Navigate To Next Screen")
}
}
}
Here's MainViewModel :
class MainViewModel : ViewModel() {
var _howManyPlayers = MutableStateFlow(0)
var howManyPlayers = _howManyPlayers.asStateFlow()
fun Selection(players:Int) {
_howManyPlayers.value = players
}
}
And here's the TwoPlayerGame Composable :
@Composable
fun TwoPlayerGame(navController: NavController, viewModel: MainViewModel) {
val howManyPlayers by viewModel.howManyPlayers.collectAsState()
Column {
Row(Modifier.padding(50.dp)) {
Text(text = "Two Player Game")
}
Row {
Text(text = "Value of howManyPlayers in viewmodel = ${howManyPlayers}")
}
}
}
When the user selects how many players in the PlayerSelection screen, the HowManyPlayers variable within the MainViewModel successfully changes as a result of which button is pressed by the user. I can see this working with the line:
Text(text = "Current Value Held in MainViewModel = ${HowManyPlayers}")
That's pretty cool. However when I Click the button to navigate to TwoPlayerGame the variable HowManyPlayers within the MainViewModel is again reset to 0. Why is the HowManyPlayers value within the MainViewModel resetting to 0 on navigating to a new screen? Is there a way to pass a ViewModel to different screens while retaining variable values within the passed ViewModel?
Thank you for considering my question, I really appreciate it!
There is two way to share a data.
First, You can pass arguments by using Navigation Arguments. Pass some value and get the value at the Screen. https://developer.android.com/jetpack/compose/navigation#nav-with-args
Second, use the repository layer for sharing some data. Expose a flow in the repository and collect this value in the ViewModel. And then, emit some data through the flow in the repository. https://developer.android.com/topic/architecture/data-layer
Maybe, you can share data with one ViewModel by fixing your NavigationAppHost
function to pass viewModel
rather than creating new instance like below.
@Composable
fun NavigationAppHost (navController: NavController, viewModel: MainViewModel) {
NavHost(navController = navController as NavHostController, startDestination = "PlayerSelection") {
composable(Destination.PlayerSelection.route) { PlayerSelection(navController, viewModel) }
composable(Destination.TwoPlayerGame.route) {TwoPlayerGame(navController, viewModel)}
composable(Destination.ThreePlayerGame.route) {ThreePlayerGame(navController, viewModel)}
composable(Destination.FourPlayerGame.route) {FourPlayerGame(navController, viewModel)}
}
}
AAC(Android Architecture Component) recommends to use one ViewModel per one Screen. Because it holds specific UI state that can be destroyed when configuration changes occur. But, there is another opinion from the MVVM pattern like reusing ViewModel per many screens. You can reference AAC ViewModel, MVVM ViewModel
Here are good examples to learn ViewModel and Navigation with Jetpack Compose.
https://github.com/android/socialite
https://github.com/android/nowinandroid [ADVANCED]