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?
CodePudding user response:
If you want to process each someUIState
value only once, you should put it inside a LaunchedEffect
and pass someUIState
as the key so that whenever it changes the block is retriggered.
val someUIState by viewModel.someUIState.collectAsState()
LaunchedEffect(someUiState) {
when(someUiState) {
// Same as in the question
}
}
Alternatively, you could just collect the flow inside a LaunchedEffect
.
LaunchedEffect(Unit) {
viewModel.someUIState.collect { uiState ->
when(uiState) {
// Same as in the question
}
}
}
CodePudding user response:
While you can use the solution provided by Arpit, I personally prefer to manage the state of the API call in the viewmodel. It is easy to abuse LaunchEffect. Also, LaunchEffect - in my opinion - should really be UI related stuff and not for handling API calls to some backend. Since you already have a variable for handling state - someUIState
- only make the API call when the state is set to Nothing
. Something like this:
class FancyViewModel() : ViewModel() {
private val _someUIState = MutableStateFlow<FancyWrapper>(FancyWrapper.Nothing)
val someUIState: StateFlow<FancyWrapper> = _someUIState
fun attemptAPICall() = viewModelScope.launch {
if (_someUIState.value != FancyWrapper.Nothing) {
return
}
_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!")
}
}
}