DamianReeves
02/13/2025, 5:00 PMStateFlow<T>
that doesn't go on forever? I'm thinking of having something like a MutableStateFlow
that allows the emission of a done (or even an error) that indicates the otherwise hot flow will no longer emit values.Zach Klippenstein (he/him) [MOD]
02/13/2025, 5:04 PMDamianReeves
02/13/2025, 5:07 PMDamianReeves
02/13/2025, 5:08 PMZach Klippenstein (he/him) [MOD]
02/13/2025, 5:09 PMkevin.cianfarini
02/13/2025, 5:09 PMsealed interface SomeState {
data object Complete : SomeState
data class Incomplete(val value: Int) : SomeState
}
val stateFlow = MutableStateFlow(Incomplete(0))
stateFlow.takeWhile { it != SomeState.Complete }.collect { ... }
DamianReeves
02/13/2025, 5:12 PMkevin.cianfarini
02/13/2025, 5:13 PMDamianReeves
02/13/2025, 5:14 PMStateFlow
always has a value I'm thinking how I can safely return the last value, in your example its the last Int valuekevin.cianfarini
02/13/2025, 5:18 PMtakeWhile
completes would be the latest incomplete state.kevin.cianfarini
02/13/2025, 5:18 PMDamianReeves
02/13/2025, 5:19 PM// Core type aliases for clarity and flexibility
typealias ModelTransformation<M> = (M) -> M
typealias IntentHandler<M, I> = (I) -> ModelTransformation<M>
typealias ViewProjection<M, V> = (M) -> V
// Model interface representing the state container
interface Model<M> {
val state: StateFlow<M>
val value:M = ???
fun update(transformation: ModelTransformation<M>)
companion object {
fun <M> create(initial: M, updates: Flow<ModelTransformation<M>>): Model<M> =
FlowBasedModel(initial, updates)
}
}
DamianReeves
02/13/2025, 5:21 PMvalue
in a way that doesn't lead to races... the most straightforward way with a StateFlow
is to use its value, but given the above strategy the last "completed" value has no value so it seems I'd need to make value
internally mutable, but that seems like it would expose me to races possiblyZach Klippenstein (he/him) [MOD]
02/13/2025, 5:25 PMkevin.cianfarini
02/13/2025, 5:25 PMStateFlow.update { old -> new }
kevin.cianfarini
02/13/2025, 5:26 PMDamianReeves
02/13/2025, 5:27 PMDamianReeves
02/13/2025, 5:29 PMsealed interface ValueOrDone<T> {
data class Value<T>(val value:T):ValueOrDone<T>
data object Done:ValueOrDone<Nothing>
}
I want to be sure I can get the last T
after a Donekevin.cianfarini
02/13/2025, 5:29 PMinterface Model<M> {
fun update(block: (old: M) -> M)
fun markComplete()
fun flow(): Flow<M>
}
class StateFlowModel<M>(initialValue: M) {
private val sf = MutableStateFlow<SomeState<M>>(initialValue)
override fun update(block: (old: M) -> M) {
sf.update(block)
}
override fun markComplete() {
sf.value = SomeState.Complete
}
override fun flow(): Flow<M> = sf.takeWhile { it != SomeState.Complete }
}
DamianReeves
02/13/2025, 5:30 PMDamianReeves
02/13/2025, 5:30 PMDamianReeves
02/13/2025, 5:30 PMDamianReeves
02/13/2025, 7:49 PMkevin.cianfarini
02/13/2025, 7:53 PMDamianReeves
02/13/2025, 9:15 PMDamianReeves
02/13/2025, 9:17 PMDamianReeves
02/13/2025, 9:19 PMDamianReeves
02/13/2025, 9:19 PMDamianReeves
02/13/2025, 9:21 PMoverride val state: Flow<M> = _state
.takeWhile { it !is CompletableState.Complete }
.map { state -> (state as CompletableState.Incomplete<M>).value }
ends up returning null if I do:
model.state.lastOrNull()
Which shouldn't be the case since I'm giving it a non-null initial valuekevin.cianfarini
02/13/2025, 9:24 PMCoroutineStart.UNDISPATCHED
or toss a yield
in before the call markCompleteDamianReeves
02/13/2025, 9:26 PMfun main() {
runBlocking {
val model = Model.create("initial state")
launch {
val currentState = model.state.lastOrNull()
println("currentState: $currentState")
check(currentState != null)
model.markComplete()
}
}
}
kevin.cianfarini
02/13/2025, 9:35 PMlastOrNull
will never complete until you can concurrently mark the model as donekevin.cianfarini
02/13/2025, 9:36 PMfun main() {
runBlocking {
val model = Model.create("initial state")
launch {
val currentState = model.state.lastOrNull()
println("currentState: $currentState")
check(currentState != null)
}
yield()
model.markComplete()
}
}
Works finekevin.cianfarini
02/13/2025, 9:37 PMfun main() {
runBlocking {
val model = Model.create("initial state")
launch(UNDISPATCHED) {
val currentState = model.state.lastOrNull()
println("currentState: $currentState")
check(currentState != null)
}
model.markComplete()
}
}
DamianReeves
02/13/2025, 9:38 PMDamianReeves
02/13/2025, 9:38 PMkevin.cianfarini
02/13/2025, 9:40 PMCoroutineDispatcher.dispatch
cycle, which in contrast queues a task to run and will not run its contents synchronouslyDamianReeves
02/13/2025, 10:40 PM