Library Version: 0.1.0
Describe the Bug:
The documentation of OnetimeWhileSubscribed says:
OnetimeWhileSubscribed is a SharingStarted strategy that ensures the upstream flow emits only once while a subscriber is active
This is incorrect. OnetimeWhileSubscribed only ensures that the coroutine collecting the upstream Flow is launched at most once, which is different.
Launching a coroutine once doesn't guarantee that the Flow it collects will have the opportunity to emit at least one value. If the coroutine is stopped before the Flow could emit at least one value (because of a slow HTTP request for example), then no value will ever be emitted because OnetimeWhileSubscribed will not start the coroutine a second time.
To go into technical details, even if the hasCollected flag is set after the suspending function emit(SharingCommand.START) returns, it doesn't mean that at that point a value has reached the StateFlow/SharedFlow cache downstream, because some Flow operators like buffer() or mapLatest() are buffered: in case of buffering, when emit() returns the emitted value is in a downstream buffer but has not yet been processed further down the chain.
In summary, the current implementation doesn't work reliably and should not be used.
Expected Behavior:
Instead of using a custom SharingStarted strategy like OnetimeWhileSubcribed acting upstream, the onetimeStateIn() implementation should collect the upstream Flow directly and only set the hasCollected flag after emitting the value directly to the downstream SharedFlow/StateFlow. This is the only reliable way to detect that the upstream Flow emitted at least one value and that this value has reached the SharedFlow/StateFlow cache.
Something like this:
fun <T> Flow<T>.onetimeStateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T,
): StateFlow<T> {
var hasCollected = false
return flow {
if (!hasCollected) {
collect {
emit(it)
hasCollected = true
}
}
}.stateIn(
scope = scope,
started = started,
initialValue = initialValue
)
}
Or better, set the flag after the upstream Flow completes (similarly to what the LiveData coroutine builder does):
fun <T> Flow<T>.onetimeStateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T,
): StateFlow<T> {
var hasCollected = false
return flow {
if (!hasCollected) {
collect(this)
hasCollected = true
}
}.stateIn(
scope = scope,
started = started,
initialValue = initialValue,
)
}
Library Version: 0.1.0
Describe the Bug:
The documentation of
OnetimeWhileSubscribedsays:This is incorrect.
OnetimeWhileSubscribedonly ensures that the coroutine collecting the upstream Flow is launched at most once, which is different.Launching a coroutine once doesn't guarantee that the Flow it collects will have the opportunity to emit at least one value. If the coroutine is stopped before the Flow could emit at least one value (because of a slow HTTP request for example), then no value will ever be emitted because
OnetimeWhileSubscribedwill not start the coroutine a second time.To go into technical details, even if the
hasCollectedflag is set after the suspending functionemit(SharingCommand.START)returns, it doesn't mean that at that point a value has reached theStateFlow/SharedFlowcache downstream, because some Flow operators likebuffer()ormapLatest()are buffered: in case of buffering, whenemit()returns the emitted value is in a downstream buffer but has not yet been processed further down the chain.In summary, the current implementation doesn't work reliably and should not be used.
Expected Behavior:
Instead of using a custom
SharingStartedstrategy likeOnetimeWhileSubcribedacting upstream, theonetimeStateIn()implementation should collect the upstream Flow directly and only set thehasCollectedflag after emitting the value directly to the downstreamSharedFlow/StateFlow. This is the only reliable way to detect that the upstream Flow emitted at least one value and that this value has reached theSharedFlow/StateFlowcache.Something like this:
Or better, set the flag after the upstream Flow completes (similarly to what the
LiveDatacoroutine builder does):