I've seen some Jetpack Compose projects and I've seen two types of managing states, not realizing which one is better.
For example, let's assume: the input state. I've seen people manage this state in the UI, using remember to save the state of the value.
Another way I've seen is to create this mutableState in the ViewModel and store/use it from there. What's the best way to do this?
In addition to @Thracian's answer.
Let me share my thought process based on my current level of experience in Jetpack Compose. Just a disclaimer, I'm still in the learning curve.
IMO, there's no such thing as "best", things in our field evolves, what might be considered "best" today may become obsolete tomorrow, but there are certain practices that are "recommended", approved and adopted by the community which might save you from dealing with some pitfalls (e.g unwanted re-compositions, infinite navhost calls(you already dealt with this) etc..), but it's up to you if you will follow it or not.
So what you're trying to understand is called State Hoisting. The way I could explain this is by just simply sampling a scenario (again this is based on my own experience with how I apply my knowledge in Jetpack Compose).
Consider a Login use-case with 3 different levels of complexity
At this point, you already have an idea the different levels of state management based on the use-case above.
For a Login prototype, I won't be needing a state class or a view model, since its just a prototype
@Composable
fun LoginScreen() {
val userName by remember { <mutable string state username> }
val password by remember { <mutable string state password> }
Column {
Text(text = username)
Text(text = password)
Button("Login")
}
}
and because its a very simple UI(composable), I only need to specify basic structure of a composable using remember + state, showcasing an input is happening.
For the Login mock-up with simple validation, we utilized the recommended state hoisting using a class,
class LoginState {
var event;
var mutableUserNameState;
var mutablePasswordState;
fun onUserNameInput() {...}
fun onPasswordInput() {...}
fun onValidate() {
if (not valid) {
event.emit(ShowToast("Not Valid"))
} else {
event.emit(ShowToast("Valid"))
}
}
}
@Composable
fun LoginScreen() {
val loginState by remember { LoginState }
LaunchedEffect() {
event.observe {
it.ShowToast()
}
}
Column {
Text(text = loginState.mutableUserNameState, onInput = { loginState.onUserNameInput()} )
Text(text = loginState.mutablePasswordState, onInput = { loginState.onPasswordInput()} )
Button(loginState.onValidate)
}
}
Now for a full blown Login Module, where you're also taking lifecylce scopes into consideration
class LoginViewModel(
val userRepository: UserRepository // injected by your D.I framework
): ViewModel {
var event;
var mutableUserNameState;
var mutablePasswordState;
fun onUserNameInput() {...}
fun onPasswordInput() {...}
fun onValidateViaNetwork() {
// do a non-blocking call to a server
viewModelScope.launch {
var isUserValid = userRepository.validate(username, password)
if (isUserValid) {
event.emit(ShowToast("Valid"))
} else {
event.emit(ShowToast("Not Valid"))
}
}
}
}
@Composable
fun LoginScreen() {
val userNameState by viewModel.mutableUserNameState
val passwordState by viewModel.mutablePasswordState
LaunchedEffect() {
event.observe {
it.ShowToast()
}
}
Column {
Text(text = userNameState, onInput = { viewModel.onUserNameInput()} )
Text(text = passwordState, onInput = { viewModel.onPasswordInput()} )
Button(viewModel.onValidateViaNetwork)
}
}
Again, this is just based on my experience and how I decide on hoisting my states. As for the snippets I included, I tried to make them as pseudo as possible without making them look out of context so they are not compilable. Also mock and prototype are considered the same, I just used them in conjunction to put things into context.