https://kotlinlang.org logo
Title
d

dave08

04/25/2023, 12:11 PM
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

CLOVIS

04/25/2023, 12:13 PM
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

dave08

04/25/2023, 12:17 PM
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

CLOVIS

04/25/2023, 12:19 PM
But then, your StateFlow is frozen (doesn't get updated anymore) when someone is not running your function that updates it
d

dave08

04/25/2023, 12:20 PM
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

CLOVIS

04/25/2023, 12:22 PM
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

dave08

04/25/2023, 12:23 PM
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

CLOVIS

04/25/2023, 12:23 PM
You can just add it in your producer function, if you always want it
fun yourFlow() = flow {
    …
}.onStart { … }
d

dave08

04/25/2023, 12:25 PM
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

CLOVIS

04/25/2023, 12:26 PM
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

dave08

04/25/2023, 12:29 PM
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

CLOVIS

04/25/2023, 12:31 PM
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

dave08

04/25/2023, 12:35 PM
So it's stateIn then... 🙄
Thanks!
c

CLOVIS

04/25/2023, 12:37 PM
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

dave08

04/25/2023, 12:38 PM
Or
data class NonEmptyFlow<T>(val firstState: T, val flow: Flow<T>)
🤒
c

CLOVIS

04/25/2023, 12:39 PM
Or, if you really want it to be a new type
@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

dave08

04/25/2023, 12:43 PM
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

CLOVIS

04/25/2023, 12:44 PM
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

dave08

04/25/2023, 12:45 PM
Well anyways, it's a pretty good start, thanks a lot!
g

gildor

04/25/2023, 3:41 PM
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

dave08

04/25/2023, 3:47 PM
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

gildor

04/25/2023, 3:49 PM
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

CLOVIS

04/25/2023, 3:51 PM
I'm really not a fan of
stateIn(GlobalScope)
because you lose all control over the running operation / memory freeing
g

gildor

04/25/2023, 3:52 PM
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