Still sorta new to flows. so maybe a basic questio...
# flow
c
Still sorta new to flows. so maybe a basic question. But I have a StateFlow and when I call map on that StateFlow I get a Flow back... but I'd prefer a StateFlow back. How would I go about doing that? Is there some extension method I don't know about?
j
You can use
stateIn
to convert a
Flow
to a
StateFlow
.
m
Unfortunately, No there isn't https://github.com/Kotlin/kotlinx.coroutines/issues/2631 keep an eye on this issue for tracking updates on this & also for possible solutions, one of those solutions is explained well in this article: https://blog.shreyaspatil.dev/combining-stateflows-and-transforming-it-into-a-stateflow
c
Interesting. This stateFlow I'm consuming didn't come from me, so I'm a bit lost on what coroutineScope I'd pass in as well as what SharingStarted to use... but ima look into it.
i wonder if the extension method at the bottom of this would work https://github.com/Kotlin/kotlinx.coroutines/issues/2514
i gotta do some more reading on this. i still dont get why mapping from a stateflow to a stateFlow isn't something thats possible. feel like im missing something basic here.
m
Interesting. This stateFlow I'm consuming didn't come from me, so I'm a bit lost on what coroutineScope I'd pass in as well as what SharingStarted to use... but ima look into it.
That depends on your use-case, how long should the flow stay in memory? For example, if you have a stateflow in an annotated @HiltViewModel
ViewModel
that holds the state of the current screen displayed, then you would use
viewModelScope
as a CoroutineScope and
SharingStarted.Lazily
to stop sharing when the scope is cancelled (e.g when the destination is popped off from back stack)
i wonder if the extension method at the bottom of this would work
Haven't tried it honestly, but I think it should work. You can also take a look at the other solutions mentioned in the issue I linked above
b
we use the following ext:
Copy code
fun <T, R> StateFlow<T>.mapAsState(mapper: (T) -> R): StateFlow<R> = object : StateFlow<R> {
    override val replayCache: List<R>
        get() = this@mapAsState.replayCache.map(mapper)

    override val value: R
        get() = mapper(this@mapAsState.value)

    override suspend fun collect(collector: FlowCollector<R>): Nothing {
        map(mapper).distinctUntilChanged().collect(collector)
        error("StateFlow.collect returned normally")
    }
}
it doesn't require any coroutine scope as it won't cause an extra collection but will rely on either stateflow's scope (to get value/replayCache) or the collector scope (when calling collect) So outside it works as simple as it could be:
Copy code
val stateFlow: StateFlow<Int> = MutableStateFlow(0)
val mapped: StateFlow<Int> = statFlow.mapAsState { it * 2 }
c
@MR3Y I guess I'm just confused on why i have to provide a scope. the stateflow that is initially created doesn't have to attach itself to a scope (maybe it does and i just have inexperience with creating stateFlows lol) but thanks for all of the info. i have some time today to read through the docs some more.
@bezrukov thanks for that. ill consider it as well!
m
You need to provide a scope to adhere to Structured Concurrency https://kotlinlang.org/docs/coroutines-basics.html#structured-concurrency, so that the flow collection doesn't leak and would be cancelled when the parent scope got cancelled.
c
how do i just reuse the scope of the initial StateFlow though?
c
how do i just reuse the scope of the initial StateFlow though?
You can use any scope, so yes, you can if you want.
the stateflow that is initially created doesn't have to attach itself to a scope
A StateFlow by itself doesn't do any operation, it just stores a value. It's a kind of observable variable, it doesn't do anything, and thus needs no scope. When using
map
, you are creating a pair of the original StateFlow + the operation to run to create new values. That operation is
suspend
, and thus needs a scope:
stateIn
is not a scope for the created StateFlow, it's a scope for all operations before it.
👍 1
I'm curious why you want to convert a Flow returned by
map
into a StateFlow. Any reason in particular? It should be rarely needed.
c
In one of my modules I have a StateFlow of Person. Person has a
name: String
All is good in the world. In another gradle module (that I'm now working on) I need a StateFlow of
name
. So originally I was passing StateFlow<Person> but since person is defined in another module I had to add that module as a dep to this module. My team lead yelled at me. so she told me to instead of passing the full heavy type of Person, to just map the name and pass a StateFlow of String/name. Hopefully that makes sense. tldr. I have StateFlow<Person> that is created/managed by someone else. I need a StateFlow<String> (Person.name) and so I feel "wrong" assigning it a scope etc. I already have a stateflow. I just wanna make it so I don't need my other module to depend on a new type.
j
If your map operation doesn't suspend, it doesn't really matter the scope you pass the
stateIn
function. If you're doing this in a viewmodel, it makes sense to use the viewmodel's scope either way.
🤯 1
c
Maybe JB should just add a
StateFlow.map(): StateFlow
variant that doesn't suspend, since indeed it's safe to create a stateflow that way
maybe that would be a welcome contribution
c
So what would be the extension function to have that @CLOVIS?
Copy code
fun <T, M> StateFlow<T>.map(
    coroutineScope : CoroutineScope,
    mapper : (value : T) -> M
) : StateFlow<M> = map { mapper(it) }.stateIn(
    coroutineScope,
    SharingStarted.Eagerly,
    mapper(value)
)
?
c
I think this works:
Copy code
private class MappedStateFlow<T, U>(
    private val input: StateFlow<T>,
    private val transform: (T) -> U,
) : StateFlow<U>, SharedFlow<U> by input.map(transform) {
    override val value get() = transform(input.value) 
}
However, the StateFlow interface is not stable for inheritance, so this wouldn't be safe in your own code. It would only be safe if added to KotlinX.Coroutines directly.
🤔 1