I am facing a weird issue where composable block only gets the latest state from a state flow. Suppo...
d
I am facing a weird issue where composable block only gets the latest state from a state flow. Suppose I change state from
A->B
and
B->C
in succession, then the composable function will only get the update of state as
C
, it will never get the state
B
, whereas in every other place if I collect the state flow, then I will get 2 state updates,
B
and
C
. Is this an intended behaviour or some bug? Code attached in thread.
VIEWMODEL code:
Copy code
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {

    private val _stateFlow = MutableStateFlow<MainState>(MainState.A)
    val stateFlow = _stateFlow.asStateFlow()

    init {
        Log.d("debug_log", "init called for VM")
        viewModelScope.launch {
            _stateFlow.collect {
                Log.d("debug_log", "got state in VM $it")
            }
        }
    }

    fun updateStates() = viewModelScope.launch(Dispatchers.Default) {
        Log.d("debug_log", "changing state")
        runCatching {
            _stateFlow.update {
                MainState.B
            }
            _stateFlow.update {
                MainState.C
            }
        }.getOrElse {
            Log.e("debug_log", "exception", it)
        }
    }
}
ACTIVITY code:
Copy code
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val mainViewModel by viewModels<MainViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            mainViewModel.stateFlow.collect {
                Log.d("debug_log", "got state in lifecycle collection $it")
            }
        }
        setContent {
            DependenciesTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    val state = mainViewModel.stateFlow.collectAsState().value

                    Log.d("debug_log", "got state in compose $state")

                    Text(text = "State is $state")

                    LaunchedEffect(true) {
                        delay(2000)
                        mainViewModel.updateStates()
                    }
                }
            }
        }
    }
}
Copy code
sealed class MainState {
    object A : MainState()
    object B : MainState()
    object C : MainState()
}
logs:
Copy code
D/debug_log: init called for VM
D/debug_log: got state in lifecycle collection com.dependencies.MainState$A@92eabdc
D/debug_log: got state in VM com.dependencies.MainState$A@92eabdc
D/debug_log: got state in compose com.dependencies.MainState$A@92eabdc
D/debug_log: changing state
D/debug_log: got state in lifecycle collection com.dependencies.MainState$B@9359876
D/debug_log: got state in VM com.dependencies.MainState$B@9359876
D/debug_log: got state in lifecycle collection com.dependencies.MainState$C@18a0e77
D/debug_log: got state in VM com.dependencies.MainState$C@18a0e77
D/debug_log: got state in compose com.dependencies.MainState$C@18a0e77
s
This is the expected behavior of
StateFlow
. If you need in-between states then you’re not modeling
state
but something else. And you may be interested in
SharedFlow
instead.
StateFlow
exists to represent state, and if in a state there’s a new state overriding the old state, there’s no reason to be notified about in-between states.
p
StateFlow is a conflated flow, if a consumer processes events slower than a producer emits them, the events that are not consumed will be overwritten. It is made to represent State.
d
Got it, legit reasons, that means that if there is some business logic (like cleanup) on a particular state, then that probably needs to be done separately using a side effect
We cannot rely on state updates for doing them
s
Yes that doesn't sound like state, so it can't be modeled with StateFlow
p
If you need to send an event C while you are on State B, you can provide a SharedFlow field in you State B. Then the view can consume this sharedFlow field that you would use to send C. Keep in mind, with share flow to guarantee not to lose events you gotta use emmit from a coroutine scope, if using tryEmit make sure you have extra buffer capacity set not zero. Otherwise the same slow consumer fast producer will bite you again.