Why does `.map` on a `StateFlow` returns a `Flow` ...
# coroutines
f
Why does
.map
on a
StateFlow
returns a
Flow
?
i
because
.map
defined as extension to
Flow
?
f
Fair enough, but why? what If I want to
.map
a StateFlow and still keep it a StateFlow
i
Write your own
map
that creates new instance of
StateFlow
and return it explicitly
But imho if you do that than entropy level in the world will be increased a bit )
map usually operates onto the flow (not the
Flow
, just flow), so it must be referenced to
Flow
abstraction and returns
Flow
, but not an subtype of
Flow
I would ask you what do you do? And why do you need the same function?
f
Defenitely, thanks for replying btw.. this is what I’m doing
Copy code
class MainViewModel {

   val sf : StateFlow = ...

   fun observeFooEvents() : StateFlow = sf.map { it is Foo }
   fun observeBarEvents() : StateFlow = sf.map { it is Bar }

}
i
You should return
Flow
from these functions
user of
observeFooEvents
and
observeBarEvents
must don’t care about kind of abstraction in your ViewModel
It should care about emitting values
This is exactly what Flow do
f
I could, but the client the needs to use it like this
Copy code
viewModel.observeBarEvents.collectAsState(initial = ???)
it asks me for an initial state
which I already had in the first place! 😄 (the stateflow must have an initial value, but it got converted to Flow)
with StateFlow,
collectAsState
doesn’t ask for an initial state
i
because state flow has initial value )
f
Exactly, that’s why I want to keep it a StateFlow
because IT IS a state flow, it just makes more sense iMO
i
However you doesn’t need state flow in UI, because UI might emit the value to state flow
I don’t know how it calls in English, but it is bad
because you get open system at this point, but it must stay closed
f
would it? … it’s not a MutableStateFlow
that would be bad
but StateFlow won’t allow to emit
i
Hm. Maybe you’re correct. But i’d find a way to pass initial value from view model
Maybe with method like
Copy code
fun <T, R> StateFlow<T>.observeAsState(mapper: (T) -> R) {
    return map(mapper).observeAsState(mapper(value))
}
Mapper can be defined in view model and logic will be incapsulated inside
sf.observeAsState(vm::isFoo)
f
let me see… What is
value
in this case?
actually let me try it
i
value is initial value in stateflow. Is it accessible?
a
what is the
...
in
val sf : StateFlow = ...
?
if it's a
MutableStateFlow
or other hot data source you might be better off using snapshot state here instead of StateFlows. More conversation on this specific use case here: https://github.com/Kotlin/kotlinx.coroutines/issues/2631
l
map
returns a
Flow
no matter what because its lambda can suspend. The best it could statically do, unless you can have non suspending lambda overloads, is return a
SharedFlow
.
☝️ 1
a
that, plus there's a whole lot of implications of keeping each intermediate value along a chain of observable value holders
f
@iamthevoid
value
is accessible! but
collectAsState
is only available on a
Composable
context @Adam Powell
is a
MutableStateFlow
in deed. Will explore using SnapShotstate instead
i
You can add
@Composable
annotation to your function🤷
Anyway it will be calling from composable context
f
True! that would work… but the mapping needs to happen in the View (composable)
I’d rather have the mapping happening in the VM
i
You can do it inside vm, example
a
if the source of the data is a
MutableStateFlow
then using snapshot state instead this simplifies to:
Copy code
class MainViewModel {
    var source by mutableStateOf(...)
        private set 
    val isFoo: Boolean get() = source is Foo
    val isBar: Boolean get() = source is Bar
}
any reads of
isFoo
or
isBar
are observable. Instead of using
collectAsState
you can just write
vm.isFoo
without any special collect calls or mapping operators
(do note the
get() =
parts in that example though 🙂 )
f
@iamthevoid Could, work, the view is still too smart for my taste (knows which mapper to use). Also there’s a lot of back and forth. I’d like the view to just hook up to the VM and start listening. @Adam Powell exploring usiing
mutableStateOf
instead will see how it goes
thank you both btw 🙂
👍 2
a
cc @Manuel Vivo for general topic interest 🙂
f
uhm
mutableStateOf
won’t cut it for me either (I think) The example I show above is not actually what I’m doing, this is what I’m actually doing
Copy code
class MainVm {
  
   val stateFlow = ...

   val fooHandler = FooHandler(stateFlow.map { it is Foo })
   val batHandler = BarHandler(stateFlow.map { it is Bar })
}
a
can you post the code for FooHandler/BarHandler?
a simple lambda type might do the trick there, e.g.
() -> Boolean
, leading to
Copy code
val fooHandler = FooHandler({ state is Foo })
but what other support you might want/need depends on what else FooHandler does with it
(and more importantly, when it does it)
f
not sure it’ll help but here it is (this is BarHandler)
val filterFlow: StateFlow<Map<FILTER, FloatArray>>
Each contextual gets passed a set of filters (it’s an image editing app)
Example, the
LightContextual
gets passed down “EXPOSURE” “BRIGHTNESS”
a
I don't see the mappings, is that what you're trying to move into the viewmodel class?
heres the ViewModel with the Mappings
maybe not it makes more sense 😅
ignore the
stateIn
.. I was trying something out
a
I see
f
updated the gist*
a
try:
Copy code
open class SliderContextualViewModel(
    private val getFilters: () -> Map<FILTER, FloatArray>,
    val onFilterChanged: (FILTER, FloatArray) -> Unit,
    val onFilterCommitted: () -> Unit = {}
) {
    val filters: Map<FILTER, FloatArray>
        get() = getFilters()
}
then use it like
Copy code
// note: mutableStateMapOf is a snapshot state map
private var appliedFilters = mutableStateMapOf<FILTER, FloatArray>()

val lightContextualViewModel = SliderContextualViewModel(
    onFilterChanged = ::onValueChanged,
    getFilters = { appliedFilters.filterKeys { listOf(FILTER.EXPOSURE, FILTER.GRAIN, FILTER.VIBRANCE).contains(it) } },
    onFilterCommitted = history::onCommit
)
👀 1
m
I just checked the Compose Architecture doc and we should show more code in general. In particular, I think we should show a ViewModel-like class that holds the UI state using
mutableStateOf
instead of `StateFlow`/`LiveData`. If you’re not working on a hybrid screen, using the latter makes no sense to me 🙂
I’ll talk with the doc’s owner to see if we can make these improvements
🚀 4
f
@Adam Powell worked first try 🦜
thanks for the help! What kind of magic is this, I definitely need to dig deeper into whats making this work
🪄 3
if this is not a lambda, it breaks
private val getFilters: () -> Map<FILTER, FloatArray>
a
@Zach Klippenstein (he/him) [MOD] did a really approachable writeup on it: https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn
👀 1
f
ViewModel-like class that holds the UI state using 
mutableStateOf
 instead of `StateFlow`/`LiveData`. If you’re not working on a hybrid screen, using the latter makes no sense to me
@Manuel Vivo what do you mean by hybrid in this context?
m
Meant the case of a screen that uses both Compose and the Android View system. As you could be in a migration phase, you’d likely have a AAC ViewModel class that is in charge of handling the View logic and usually exposes a stream of UI state using
StateFlow
or
LiveData
. So if you’re mixing both approaches, you could also consume that stream for Compose. But if you’re in a Compose-only screen, you could just use the Compose State APIs.
f
Gotcha, makes sense thanks!