Avoid race conditions in StateFlow

image.png

Nowadays, when everyone is trying out the great Jetpack Compose, it is most likely that you will use the MVI architectural pattern and expose one single state object from the ViewModel to an Activity. The beauty of this is there is a single source of truth for UI State. You will observe it through StateFlow a Kotlin Flow API, and to represent the emitted states you modify the views accordingly.

To modify the UI states, a lot of the devs are using mutableState.value = mutableState.value.copy() assignment. This can lead to some abnormal state emissions.

Let us go through an example.

data class UIState(
    val listA: List<Any> = emptyList(),
    val listB: List<Any> = emptyList()
)

Let say we have this state class which contains two lists, Now we let say have two asynchronous functions,

private fun editListA() {
        viewModelScope.launch {
            val newList = Api.getList()
            mutableState.value = mutableState.value.copy(
                listA = newList
            )
        }
    }

private fun editListB() {
        viewModelScope.launch {
            val newList = Api.getList()
            mutableState.value = mutableState.value.copy(
                listB = newList
            )
        }
    }

These functions are loading lists from some Api, and then updating the current UI state with these lists.

Now what if these functions executes at almost same time?

Let’s think about this for a second. If both functions finish at almost the same time, then there is some certainty that the first function has captured the current state value, and it is now doing a copy, but before assigning the newly created state to the current state, the second function also tries to modify the current state. This will result in an inaccurate UI State, because the second function will finish later, and it captured the initial value of the state and not the modified version which contains the edited list.

How to fix this race condition?

There is a great extension function by Kotlin for MutableStateFlow which helps us in these kinds of situations.

public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
    while (true) {
        val prevValue = value
        val nextValue = function(prevValue)
        if (compareAndSet(prevValue, nextValue)) {
            return
        }
    }
}

Updates the MutableStateFlow.value atomically using the specified function of its value function may be evaluated multiple times, if value is being concurrently updated.

As you can see, we can update our current state atomically with the update() function and prevent the race condition.

To fix the issue, we need to modify our assignments to -

private fun editListA() {
        viewModelScope.launch {
            val newList = Api.getList()
            mutableState.update { value ->
                value.copy(
                    listA = newList
                )
            }
        }
    }

private fun editListB() {
        viewModelScope.launch {
            val newList = Api.getList()
            mutableState.update { value ->
                value.copy(
                    listB = newList
                )
            }        
        }
    }

TLDR

When your StateFlow value assignment depends on the previous value, use the mutableState.update { previousState -> previousState.copy() } to avoid race conditions.

Thank you for reading, Stay tuned for such informative tips 😃