'MutableStateFlow force update / notify collector

MutableStateFlow doesn't notify collectors if the updated value equals the old value (source). I've found a workaround for this, but it doesn't scale well for complex values.

Workaround: Duplicate data classes with copy() and lists with toList()/toMutableList().

Example 1: Simple data class WorkoutRoutine using workaround to rename name. Nothing wrong here.

data class WorkoutRoutine(
    var name: String,
)

val workoutRoutine = MutableStateFlow(WorkoutRoutine("Initial"))
                                                                                           
workoutRoutine.value.name = "Updated" // Doesn't notify collectors
                                                                                           
workoutRoutine.value = workoutRoutine.value.copy(name = "Updated") // Workaround: works

Example 2: Complex data class WorkoutRoutine with multiple dependencies, using workaround to add a Set to an Exercise in the WorkoutRoutine: This requires a lot of copy() and toMutableList() calls, which make the code unreadable.

data class WorkoutRoutine(
    var name: String,
    var exercises: MutableList<Exercise> = mutableListOf(Exercise())
)
                                                                         
data class Exercise(
    var sets: MutableList<Set> = mutableListOf(Set())
)
                                                                         
data class Set(
    var weight: Int? = null
)
                                                                         

val workoutRoutine = MutableStateFlow(WorkoutRoutine("Initial"))

// Doesn't notify collectors
workoutRoutine.value.apply {
    exercises = exercises.also {
        it[0].sets.add(Set())
    }
}

// Workaround: works
workoutRoutine.value = workoutRoutine.value.copy(
    exercises = workoutRoutine.value.exercises.toMutableList().also {
        it[0] = it[0].copy(sets = it[0].sets.apply { add(Set()) })
    }
)

I've tried the following:

  • Adding an extension value MutableStateFlow.valueNotDistinct that force updates MutableStateFlow.value.
    -> Problem: MutableStateFlow.value has to be nullable
var <T> MutableStateFlow<T?>.valueNotDistinct: T?
    get() = null
    set(newValue) {
        value = null
        value = newValue
    }
  • Using MutableSharedFlow, which doesn't check for equality
    -> Problem: Not as performant, doesn't have value property

What I want is to simply notify collectors on every emit, but I don't know how to do that, because there doesn't seem to be a "force notify" function for MutableStateFlow.



Solution 1:[1]

StateFlow documentation states this:

Strong equality-based conflation

Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one. State flow behavior with classes that violate the contract for Any.equals is unspecified.

A workaround could be overriding the equals method to always return false. So a data class doesn't help in your case.

class WorkoutRoutine() {
    ...
    override fun equals(other: Any?): Boolean {
        return false
    }    
}

Solution 2:[2]

MutableStateFlow is just an interface, so if you don't like how the default implementation works you can just write your own. Here is a simple implementation that uses a MutableSharedFlow to back it. It doesn't do the comparison, so it will always update.

class NoCompareMutableStateFlow<T>(
    value: T
) : MutableStateFlow<T> {

    override var value: T = value
        set(value) {
            field = value
            innerFlow.tryEmit(value)
        }

    private val innerFlow = MutableSharedFlow<T>(replay = 1)

    override fun compareAndSet(expect: T, update: T): Boolean {
        value = update
        return true
    }

    override suspend fun emit(value: T) {
        this.value = value
    }

    override fun tryEmit(value: T): Boolean {
        this.value = value
        return true
    }

    override val subscriptionCount: StateFlow<Int> = innerFlow.subscriptionCount
    @ExperimentalCoroutinesApi override fun resetReplayCache() = innerFlow.resetReplayCache()
    override suspend fun collect(collector: FlowCollector<T>): Nothing = innerFlow.collect(collector)
    override val replayCache: List<T> = innerFlow.replayCache
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Glenn Sandoval
Solution 2