Search code examples
androidkotlinkotlin-stateflow

Android StateFlow collection overrides UI changes


Assume there is a FilterActivity with Switch and EditText controls. The latter has input disabled but is clickable. A click on it launches TypeActivity to pick a type value from and then gets the type name populated into the EditText.

There is a FilterViewModel with a StateFlow<FilterUiState>

data class FilterUiState(
    var status: Boolean = false,
    var type: String = "sometype"
)

which is collected by the activity in onCreate like this

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect {
            binding.run {
                statusSwitch.isChecked = it.status
                typeEditText.text = it.type
            }
        }
    }           
}

The problem is if the user changes the statusSwitch state and then clicks on the typeEditText to pick a type, then on return back the FilterActivity gets resumed which triggers the uiState.collect and consequently resets the statusSwitch check state back to the initial value.

What is the right way to prevent UI changes from being overridden by StateFlow collect?


Solution

  • You need to actually update the state flow with the latest state you want it to hold, and then it will properly restore the latest state when collected.

    Also, don't use a mutable class for this. It's error prone to mix mutable classes with StateFlows. The StateFlow cannot detect that a change has occurred if you mutate the instance it is already holding.

    data class FilterUiState(
        val status: Boolean = false,
        val type: String = "sometype"
    )
    
    // in ViewModel class:
    
    private val mutableUiState = with(PreferenceManager.getDefaultSharedPreferences(context)) {
        val status = //...get these from shared preferences
        val type = //...
        val initialState = FilterUiState(status, type)
        MutableStateFlow(initialState)
    }
    val uiState = mutableUiState.asStateFlow()
    
    fun updateStatus(status: Boolean) {
        mutableUiState.value = mutableUiState.value.copy(status = status)
        // and update the SharedPreferences value
    }
    
    fun updateType(type: String) {
        mutableUiState.value = mutableUiState.value.copy(type = type)
        // and update the SharedPreferences value
    }
    

    And call these two update functions from the Activity when these values change. You can add a click listener to the checkbox to do this for the "status" and for the EditText, since you have input disabled, you can call the updateType() function instead of directly modifying its text when returning from the other Activity. Your existing collector will update the text in the widget for you when you update the state in the view model.