Is there a way to make a `StateFlow` from a callba...
# coroutines
d
Is there a way to make a
StateFlow
from a callback (the first state is queried and the callback is just to listen for changes to that first state - but I need to unregister from the callback when the flow is cancelled...)? I know there's
stateIn
, but I'd rather do it w/o having to pass a coroutine context there...
c
You can use
stateIn
. It's not possible to do it without
stateIn
, because a callback is cold, and a StateFlow is hot (= it maintains its own state even when no one is looking at it). That means it has to run somewhere, which is in a coroutine (thus it must have a
CoroutineScope
)
d
Well, If I would create a function that saves/manages an internal
MutableStateFlow
and manage the registration to the callback there... when it's run I would return the
StateFlow
, but then I'd have a problem with cancellation not unregistering.
And I'm wondering if there's a cleaner way...
And the callback could be used in a
coroutineScope { launch { } }
, to run it on the coroutine scope of the caller function (this function can be suspend)
In that case, I wouldn't need a coroutine scope passed down, but I find all that pretty messy.
c
But then, your StateFlow is frozen (doesn't get updated anymore) when someone is not running your function that updates it
d
That's fine, I just need it while it's being collected.
But I want to ensure in the function I'm passing it to that it has a first value... that's why I want a StateFlow
Sort of like NonEmptyList... where one value is always assured
Flow can just stay stuck...
c
If your goal is just to force a first value, you can use
onStart { emit("Your default value") }
on any cold flow (it will still be a cold flow, but it will have that first value instantly available) → https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/on-start.html
d
Yeah, but that won't be in the function signature... so one can make a mistake in a unit test or an alternate implementation and forget to put that onStart...
c
You can just add it in your producer function, if you always want it
Copy code
fun yourFlow() = flow {
    …
}.onStart { … }
d
Yeah, but the one calling it has a
someFlow: Flow<X>
as a parameter, which doesn't tell the implementer of that flow that a first value is required... it's like using a regular List as an input parameter to a function when you mean a NonEmptyList...
But you're probably right that I'm not using StateFlow for what it was meant for 😊
c
Isn't it worse to return
StateFlow
? In your version, it is not stateful, as its state only exists while the call to the other function is active, after which it becomes frozen forever
d
Copy code
fun provider() = MutableStateFlow<X>().apply {
  state = X()

  // register listener to return X updates

  this.asStateFlow()
}

suspend fun receiver(states: StateFlow<X>) {
   // I have an assurance that a first value is available already
   states.collect {
     //// process...
  }
}

// usage:
receiver(provider())


// In tests I'm forced to make a provider with a first value too...
c
I have a caching library which maintains a StateFlow internally and ensures it is always correctly synced with the previous cold flows (restarting it if necessary, freeing the stateflow when no subscriber is still active, etc); repository | the code It's nontrivial
I know, right 😅
d
So it's stateIn then... 🙄
Thanks!
c
Ahahah. If you have the use-case for a full-blown cache for request merging, don't hesitate to use the lib! It's lacking a bit in documentation at the moment, but it's well-tested and already used in production. But yeah, making a cold flow hot has a lot of edge cases
Honestly, in your situation, I would just use
onStart
and adds some documentation
d
Or
data class NonEmptyFlow<T>(val firstState: T, val flow: Flow<T>)
🤒
c
Or, if you really want it to be a new type
Copy code
@JvmInline
value class NonEmptyFlow<T> internal constructor(private val flow: Flow<T>) : Flow<T> by flow

fun <T> Flow<T>.withDefault(block: suspend () -> T) = this
    .onStart { emit(block()) }
    .let { NonEmptyFlow(it) }
Keep in mind that it if you write
flow.withDefault { 1 }.withDefault { 2 }.toList()
, it will print
[2, 1, <everything else>]
, so "default" is probably not the right name
d
That's pretty nice as a start, but I guess it would always be boxed when using the
by flow
, so a
value class
wouldn't help?
c
If you use it directly, I don't think so? If put it in a variable declared as
Flow<T>
, then yes, for sure
Probably still lighter than a regular class because it frees the compiler from attempting more stuff
d
Well anyways, it's a pretty good start, thanks a lot!
g
You can use stateIn with GlobalScope (or any own app level scope or class scope), when parent flow will complete, it will not use any additional resources, so I really don't see a reason trying to avoid state in But in general I agree, that StateFlow or flow in general is probably not what you really need, your case is essentially a suspend function with memoization, which I usually encode with caching lazy async, and suspend function which just calls deferred.await() If some of your code needs a flow (for example for a flow chain), you just wrap it with suspend lambda + asFlow()
d
It's not just a deferred, it's an initial state polled right away, and then sending the results of listening to a BroadcastReceiver for changes in that state... but I don't want to take a chance that the implementation will hang if the implementor doesn't send that first state... (there might not be any other states after that... but anyways the flow gets cancelled when the info isn't displayed to the user anymore...)
g
Fair enough, if you need initial value which may or may not return following value, you need Flow Still it also can be done with Flow + initial value as Ivan suggested (if you don't really use StateFlow.value), but if you need StateFlow specifically, use stateIn as a way to provide value
c
I'm really not a fan of
stateIn(GlobalScope)
because you lose all control over the running operation / memory freeing
g
Not, if the whole flow is garbage collected
I would say that it heavily depends on what kind lifecycle and control on operation you need
I also don't see why would you lose control on running operation, if stateIn is cancelled (for example WhileSubscribed and all subscribers are unsubsceibed), parent flow also will be cancelled and you will end up with StateFlow with default value
But I agree, that I would prefer Flow over StateFlow if I really don't need StateFlow specifically
268 Views