I have a ViewModel with a stateFlow
. Everytime that a button is clicked, I'm changing the value to the state but, for some reason, the screen is not reacting to that change.
I know that the state's value does change because when I turn the screen and there's a complete recomposition, it shows what it should show with the value changed, but only after turning the screen. I want the change on the screen without turning the screen.
This is my viewModel
:
@HiltViewModel
class CragToposViewModel @Inject constructor() : ViewModel() {
private var _state = MutableStateFlow(CragToposState(true))
val state = _state.asStateFlow()
}
This is my models:
data class CragToposState(
var zoomedOutImage: Boolean,
)
And this is my screen:
@Composable
fun CragToposScreen(
navController: NavController,
cragToposViewModel: CragToposViewModel = hiltViewModel(),
) {
val state = cragToposViewModel.state.collectAsState()
CragToposContent(
navController = navController,
state = state,
)
}
@Composable
fun CragToposContent(
navController: NavController,
state: State<CragToposState>,
) {
val context = LocalContext.current
val scrollState = rememberScrollState()
if (state.value.zoomedOutImage) {
// SOME OTHER STUFF HERE
ZoomButton(state)
} else {
// SOME OTHER STUFF HERE
ZoomButton(state)
}
}
@Composable
fun ZoomButton(state: State<CragToposState>) {
Box(
modifier = Modifier
.clickable { state.value.zoomedOutImage = !state.value.zoomedOutImage },
) {
Icon(
painter = if (state.value.zoomedOutImage)
painterResource(R.drawable.icon_zoom_in)
else painterResource(R.drawable.icon_zoom_out),
contentDescription = "",
)
}
}
The way I'm setting the new value to the state, is the correct way?
Do I need a StateFlow
here? I just want the state to change after pressing a button
Why do I have to use State<CragToposState>
and can't just use CragToposState
on fun CragToposContent
?
The main issue is that Compose cannot detect the changed zoomedOutImage
and therefore doesn't recompose, i.e. the UI is never updated.
There are multiple problems in your code that need to be addressed:
The StateFlow's content needs to be immutable. CragToposState
contains a var
property, that must be replaced with val
:
data class CragToposState(
val zoomedOutImage: Boolean,
)
Don't pass State
objects around, only pass their value:
@Composable
fun CragToposContent(
navController: NavController,
state: CragToposState,
) { /*...*/ }
@Composable
fun ZoomButton(
zoomedOutImage: Boolean,
) { /*...*/ }
Changes to the state must be performed by the view model:
fun toggleZoomedState() {
_state.update {
it.copy(zoomedOutImage = !it.zoomedOutImage)
}
}
Pass this function to the composables that need it:
@Composable
fun CragToposContent(
navController: NavController,
state: CragToposState,
toggleZoomedState: () -> Unit,
) { /*...*/ }
@Composable
fun ZoomButton(
zoomedOutImage: Boolean,
toggleZoomedState: () -> Unit,
) { /*...*/ }
Everything put together, your composables should look like this:
@Composable
fun CragToposScreen(
navController: NavController,
cragToposViewModel: CragToposViewModel = hiltViewModel(),
) {
val state by cragToposViewModel.state.collectAsStateWithLifecycle()
CragToposContent(
navController = navController,
state = state,
toggleZoomedState = cragToposViewModel::toggleZoomedState,
)
}
@Composable
fun CragToposContent(
navController: NavController,
state: CragToposState,
toggleZoomedState: () -> Unit,
) {
val context = LocalContext.current
val scrollState = rememberScrollState()
if (state.zoomedOutImage) {
// SOME OTHER STUFF HERE
} else {
// SOME OTHER STUFF HERE
}
ZoomButton(
zoomedOutImage = state.zoomedOutImage,
toggleZoomedState = toggleZoomedState,
)
}
@Composable
fun ZoomButton(
zoomedOutImage: Boolean,
toggleZoomedState: () -> Unit,
) {
Box(
modifier = Modifier
.clickable(onClick = toggleZoomedState),
) {
Icon(
painter = if (zoomedOutImage)
painterResource(R.drawable.icon_zoom_in)
else painterResource(R.drawable.icon_zoom_out),
contentDescription = "",
)
}
}
You need to use a StateFlow because you need an observable data structure. Compose provides MutableState
for that, but that doesn't properly work in view models. In a view model the way to go is to use a StateFlow and then convert it to a State
in your composables by calling collectAsStateWithLifecycle
(you should prefer this to collectAsState
to properly unsubscribe from the flow when it is not needed).
If you just want to toggle the zoomed state you do not even need a view model or CragToposState
, a simple Boolean, remembered and wrapped in a MutableState would suffice:
@Composable
fun CragToposContent(
navController: NavController,
) {
val context = LocalContext.current
val scrollState = rememberScrollState()
var zoomedOutImage by rememberSaveable { mutableStateOf(true) }
if (zoomedOutImage) {
// SOME OTHER STUFF HERE
} else {
// SOME OTHER STUFF HERE
}
ZoomButton(zoomedOutImage) {
zoomedOutImage = !zoomedOutImage
}
}