8

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.

Noah
  • 2,718
  • 3
  • 17
  • 23
  • stop using mutable data class, use immutable data class and `List` instead of `MutableList`, now you won't need "force" notify – EpicPandaForce Dec 20 '20 at 17:35
  • Thanks for your answer, but im not sure I understand. Making my data class properties immutable isn't going to fix the unreadable code, it's just going to force me to write code that works at compile time, because I can't modify the values without using `copy()` or `toMutableList` (which is good, but the code is still ugly). – Noah Dec 20 '20 at 20:09
  • 1
    Please refer to my answer here https://stackoverflow.com/questions/65442588/proper-way-to-operate-collections-in-stateflow/65442762#65442762 – Nikola Despotoski Dec 28 '20 at 12:01
  • 1
    Considering how you are using it in your workout example, I think the correct approach would be to indeed copy a workout routine, apply changes, and the assign it in place of an old one. Most threading related instruments, like flows, are oriented towards immutable objects, so this would be the "right" way to do it. Because if a consumer starts reading your value while you modify it in another thread, results could be hard to predict and often lead to bugs. – Deinlandel Jan 18 '21 at 13:34

3 Answers3

3

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
    }    
}
Glenn Sandoval
  • 3,455
  • 1
  • 14
  • 22
  • 1
    Thanks for your answer. This seems like bad practice though - im beginning to think StateFlow is not intended for my use case. Is there something similar that I could use - maybe LiveData? – Noah Dec 21 '20 at 07:12
  • 1
    You can and should use SharedFlow instead of StateFlow if you don't need conflation. – Roman Elizarov Dec 21 '20 at 08:11
  • Thank you, this seems like a good solution. But `SharedFlow` doesn't have a `value` property, which would be nice to have. Do I have to collect the flow every time I want to access a `SharedFlow`s current value? – Noah Dec 21 '20 at 10:13
  • 1
    This might propagate bugs on places where comparison is performed. This is not good solution. – Nikola Despotoski Dec 28 '20 at 12:02
  • Dudes, use LiveData, it's the best for this kind of situation. – Gabriel TheCode Aug 26 '22 at 08:32
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
}
John Oberhauser
  • 396
  • 1
  • 3
  • 12
0

Use MutableStateFlows update function in combination with data class copy function to safely update your StateFlow:

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

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

// Doesn't notify collectors
workoutRoutine.update { currentState ->
    currentState.copy(
        exercises = (currentState.exercises.firstOrNull()?.sets ?: emptyList()) + Set()
    }
}
Torhan Bartel
  • 550
  • 6
  • 33