Hi, I have some values that are provided to my vie...
# compose
n
Hi, I have some values that are provided to my view model as a
MutableStateFlow
. I then want to “proxy” these through my view model to the view and expose them as something that the view can bind to. I want to avoid exposing them to the view as flows and then calling
collectAsState
inside the view as i want to keep most things inside the view model.
My gut feel was to see if they could somehow be hooked up to
derivedStateOf
but can’t see how. The other option was to collect these flows in the view model and then populate
mutableState
values but these seems messy.
a
when you say they're provided to the viewmodel as
MutableStateFlow
, are they internal
MutableStateFlow
properties of the viewmodel, or passed as constructor parameters or similar?
n
they are grabbed from a repository (not internal to the view model).
a
how long do you want the subscription to live for? only while a UI is observing it, or for as long as the viewmodel exists?
collectAsState
might be the right thing here
z
I want to keep most things inside the view model
I would too, but exposing a
StateFlow
of values for the view to observe is keeping “most things” inside the view model. I think that’s widely regarded as a very reasonable boundary. As long as the values are specifically designed to be consumed by the view (might require mapping inside your view model).
a
I've been finding some really interesting breakdowns between where I use snapshot state vs.
StateFlow
in my code. Not really enough to write about yet, but I tend to lean on the former quite a bit these days, especially since
snapshotFlow {}
is around
if I have a really good scope to use for
.stateIn
I'll usually lean on
StateFlow
but I end up using
mutableStateOf
instead of
MutableStateFlow
almost every time
n
hmm so i guess it really only matters when the view is observing it. It just feels somewhat cleaner having the view model expose something the view can use straight away instead of another layer of handling in the view. @Adam Powell in regards to using mutableStateOf are you suggesting collecting the flow in the view model or changing the repository to return mutableState? I assume we want to keep mutablestate out of the repository as it is a compose construct.
a
It depends. We gave a lot of thought to breaking snapshots out of compose runtime into their own module to be considered more general
I tend to factor it in terms of hot or cold values
purely cold => flow, purely hot => snapshot state
most repositories are cold by nature so the point is kind of moot
but
MutableStateFlow
is kind of this weird case, like Rx Subjects, where it's always a bridge from hot code, and in nearly all cases I've encountered snapshot state ends up being more useful in those code paths for me
so usually whenever I reach for
MutableStateFlow
I try to push it either to snapshot state or to a cold flow with a well-defined scope for a
.stateIn
operator and I end up happier with the result
n
when you say snapshot state you are referring to
State
provided by compose yeah? So my impression is that there is no sort of
collectAsState
equivalent that can be used outside of composables so options are 1) use collect as state in composable composable (which works but adds sort of an extra layer) 2) Change the repository to return a
State
value 3) Collect the flow in the view model and populate
State
with it. That covers it right?
What were the arguments for not breaking snapshots out?
a
snapshotFlow {}
returns a
Flow
that can be used out of composition to observe the changing values of a function that returns the result of evaluating snapshot state, and the other lower level snapshot APIs may be used as well.
Also modify (2) above: "Change the repository to return the value of
State
or values computed from it" - it's not necessary to expose the actual
State
objects in the public interface of a class for snapshot state observation to work.
n
Thanks for your help on this one! Slowly forming a better picture 🙂
👍 1
z
I’m really curious how exposing properties that are implicitly reactive, backed by snapshot states somewhere, scale to large codebases. One of the advantage of expressing the fact that a value can change in the type system (i.e. by exposing a
Flow
or
StateFlow
-typed property) is that it’s self-documenting that the value is expected to change – you can see that just by looking at the type. Also, explicit stream types are useful in interfaces for communicating to implementors that the implementations should be reactive. In a world where a bunch of those have been replaced by regular old vals, my instinct is to be worried that it could become very tricky to look at code in the middle layers and figure out if things are reactive or not, leading to bugs where people forget to check, and assume that something will cause recompositions when it won’t, or where someone will implement an interface without realizing that the interface contract requires implementations to be backed by snapshot states.
a
Generally the signal for interfaces is if the interface is marked
@Stable
, but yes, you're not alone in being nervous about it. It was a source of a lot of debate with Yigit, Sergey and others on the arch components team as this was all getting off the ground.
z
What was the resolution to those discussions? Am I just stockholm-syndromed by years of RxJava/Flow? Is there some galaxy-brain thing where the new paradigm is so fundamentally better that this isn’t actually a problem in practice?
I could see the stable marker thing being potentially used to generate compiler errors/warnings in the future if you mess this up, which i think would solve some of those issues.
a
It wouldn't catch all of the potential transitive uses across lambda captures, etc. but it could probably calm a lot of nerves
The resolution so far was basically that Yigit wasn't going to fight us silly compose people about it so long as we weren't actively recommending people go snapshot-wild way in the business logic of apps as opposed to the UI. 🙂 That was a couple years ago
😅 3
I wouldn't put words in his mouth but I think he's a lot more comfortable with it these days
it's a bit self-limiting in that snapshots are exclusively hot. (They kind of have to be or else
.value
is super broken as a concept, which we learned the hard way with
LiveData
) When cold sources are still pushed toward
Flow
the question doesn't tend to come up as often
👍 1
it does leave
StateFlow
and particularly
MutableStateFlow
in a weird limbo of, "which do I use when and why?" when you're in compose-facing code, and for my own part I've found snapshots a lot easier and more pleasant to work with in places where I'd otherwise use
StateFlow
. There are certainly exceptions; you'll note that
Recomposer
uses
StateFlow
in a couple places because it was a natural choice there
the part where snapshots shine for me and flows/rx aren't even in the running is for the case of cross-object consistency. All snapshot state is consistent with one another, can be treated transactionally, and can't get out of sync.
👍 1
With flows you have to mush everything into a single flow to get a single source of truth and then break out individual data members from those single objects in that single stream. Lots of complex merging, sharing, and forking.
with snapshots, you basically have an implicit global redux store in the snapshot itself. It always updates consistently.
Objects that have no semantic reason to know about one another's existence are easy to keep consistent.
You don't need the purely structural parts that form them into a single immutable tree so you can emit them from a flow.
just refer to the natural sources of truth via object references. Take a snapshot and work within it if you need all of them to be consistent for an operation, like composition.
z
The immutability --> mutability shift is also something i’m wary of, but it seems much more likely that the snapshot mechanism addresses the core issues that made us all so scared of mutable state up to now, so i just need to chill out and learn to trust again.
I can’t wait to see what kinds of patterns emerge with this over the next few years. I think you mentioned in a different thread the other day possibly pulling the snapshot stuff into their own artifact – seems like that would enable a lot more exploring of these ideas in non-UI business code.
a
yeah it's all in its own package in runtime so we might consider it even post-1.0
the aha moment that made me a fan is that it's not really mutable in the traditional sense. It's immutable under the hood pretending to be mutable in the interface.
👍 1
😮 1
in the STM/MVCC sense
in terms of patterns, we bumped into a new one earlier this week when we were trying to solve the problem of
LazyColumn
DSL changes not being recomposed in time
consider:
Copy code
val items = remember { mutableStateListOf<String>() }
LazyColumn {
  items(items.size) { index ->
    Text("Item ${items[index]}")
  }
}
if you rely on recomposition to update that
LazyColumn
list adapter, things get really interesting if you try to programmatically scroll the list from an arbitrary user code path and suddenly there are fewer items than
items.size
returned since the last recomposition
but if you define the list adapter info as:
Copy code
val currentDslBlock by rememberUpdatedState(dslBlockParameter)
val adapter by remember { derivedStateOf { ListAdapter().apply(currentDslBlock) } }
then
derivedStateOf
will re-evaluate its expression whenever it's read - pull-based, not push-based - if any snapshot state accessed in the build process changed since last time
🤯 1
like a reactive
by lazy {}
composition isn't technically involved at all. It's all just snapshots.
using this you can write a toy navigation routing api for compose in ~50 lines of code: https://gist.github.com/adamp/634046641989bd20ac5b5255aea78a81
and the routes defined by it are fully dynamic
z
wow yea that’s impressive. very cool
a
going back to your original question of how it scales in large codebases though, frankly I don't know yet. 🙂 This is all pretty new in this context, and the only point of reference we really have is other STM/MVCC type systems. I have high hopes, and in the small to medium codebases we've seen it be effective in so far, it's very promising
but things always get way more interesting when you cross that mark of number of developers in the same codebase high enough that they don't all fit around the same lunch table anymore
the compose codebase itself is a good sign in that regard, but that's a group of people who all live and breathe compose itself all day rather than interact with it here and there as part of wider app work
z
😂 1