Reputation: 3425
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:
MutableStateFlow.valueNotDistinct
that force updates MutableStateFlow.value
.MutableStateFlow.value
has to be nullablevar <T> MutableStateFlow<T?>.valueNotDistinct: T?
get() = null
set(newValue) {
value = null
value = newValue
}
MutableSharedFlow
, which doesn't check for equalityvalue
propertyWhat 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.
Upvotes: 12
Views: 11964
Reputation: 560
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()
}
}
Upvotes: 1
Reputation: 466
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
}
Upvotes: 2
Reputation: 3745
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
}
}
Upvotes: 3