I'm doing a API call in the ViewModel and observing it in the composable like this:
class FancyViewModel(): ViewModel(){
private val _someUIState =
MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
val someUIState: StateFlow<FancyWrapper> =
_someUIState
fun attemptAPICall() = viewModelScope.launch {
_someUIState.value = FancyWrapper.Loading
when(val res = doAPICall()){
is APIWrapper.Success -> _someUIState.value = FancyWrapper.Loading(res.vaue.data)
is APIWrapper.Error -> _someUIState.value = FancyWrapper.Error("Error!")
}
}
}
And in composable, I'm listening to 'someUIState' like this:
@Composable
fun FancyUI(viewModel: FancyViewModel){
val showProgress by remember {
mutableStateOf(false)
}
val openDialog = remember { mutableStateOf(false) }
val someUIState =
viewModel.someUIState.collectAsState()
when(val res = someUIState.value){
is FancyWrapper.Loading-> showProgress = true
is FancyWrapper.Success-> {
showProgress = false
if(res.value.error)
openDialog.value = true
else
navController.navigate(Screen.OtherScreen.route)
}
is FancyWrapper.Error-> showProgress = false
}
if (openDialog.value){
AlertDialog(
..
)
}
Scaffold(
topBar = {
Button(onClick={viewModel.attemptAPICall()}){
if(showProgress)
CircularProgressIndicator()
else
Text("Click")
}
}
){
SomeUI()
}
}
The problem I'm facing is someUIState's 'when' block code in FancyUI composable is triggered multiple times during composable recomposition even without clicking the button in Scaffold(for eg: when AlertDialog shows up). Where am I doing wrong? What are the correct better approaches to observe data with StateFlow in Composable?
Other solution that I do with snackbars is informing the view model that the data has been consumed: In your FancyUI:
...
when(val res = someUIState.value){
is FancyWrapper.Loading-> showProgress = true
is FancyWrapper.Success-> {
...
viewModel.onResultConsumed()
}
is FancyWrapper.Error-> showProgress = false
}
...
And in your view model:
class FancyViewModel() : ViewModel() {
private val _someUIState = MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
...
fun onResultConsumed() {
_someUIState.tryEmit(FancyWrapper.Nothing)
}
}
EDIT
Here is another solution if someone still looking for this:
Create Event class:
/*
* Copyright 2017, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
Event class was originally created for LiveData but works fine with Flow, the value of the event will be bull if has been already consumed, this way you can save the call to the view model.
Use it in your screen:
...
when(val res = someUIState.value){
is FancyWrapper.Loading-> showProgress = true
is FancyWrapper.Success-> {
res.event.getContentIfNotHandled?.let {
//do stuff here
...
}
}
is FancyWrapper.Error-> showProgress = false
}
...
To use in a view model you have just to create an event for the state you want to show, for ex:
_someUIState.tryEmit(FancyWrapper.Success(event = Event(data)))