https://kotlinlang.org logo
#compose
Title
# compose
e

escodro

02/18/2021, 5:02 PM
Hello, everyone! 😊 I have a complex Composable function that I decided to create two ViewModels in order to better split the responsibilities and both of them emits state (
sealed classes
via
StateFlow
). How could I handle the state changes? 1. Combine both states (with
.combine
) and create a third “ViewState” mixing and matching the two states. 2. Observe both individually and let my function recompose when they change 3. Another better approach? The first ViewModel has three states: EmptyList, LoadedList, Error and the second one has two: DataLoaded and Error. Thanks and advance! 🎉
👍 1
a

Adam Powell

02/18/2021, 5:07 PM
If you use
[Mutable]State<T>
instead of
StateFlow
you get snapshot consistency of both plus observability for free
e

escodro

02/18/2021, 5:14 PM
Sorry, I don’t know if I get it. Replacing
StateFlow
to
State
and observing both will be better than doing so with
StateFlow
?
c

Casey Brooks

02/18/2021, 5:16 PM
I’d say it depends on the relationship between the two ViewModels. If they are independant of one another, then it probably makes the most sense to observe each one individually (
.collectAsState()
). If they are related, I like to choose one as the “parent” VM and collect that one in the “child” VM, and only
collectAsState()
on the child VM (basically making a hierarchy of VMs, rather than a loose network of them which are collected in various places). The child VM emits a state object which includes the necessary properties from its parent (with any necessary transformations), as well as its own state
a

Adam Powell

02/18/2021, 5:26 PM
I'd just use
by mutableStateOf
properties on the class, yeah. And
withMutableSnapshot {}
whenever several things need to change as part of a single snapshot transaction
Snapshot observation is transitive, no need to explicitly start a subscription operation. If anything about the class reads snapshot state the observe is recorded
So all of the business about combining states just becomes calling normal functions and reading properties.
e

escodro

02/18/2021, 5:38 PM
Something like this?
Copy code
val detailViewState by detailViewModel.state
val categoryViewState by categoryViewModel.state

withMutableSnapshot {
    TaskDetailRouter(
        detailViewState = detailViewState,
        categoryViewState = categoryViewState
    )
}
a

Adam Powell

02/18/2021, 6:08 PM
the
withMutableSnapshot
is used when you're actually performing mutations that should be seen as atomic
you don't need it when creating an instance of the object
and if you're performing these operations from the main thread in the global transaction, that's fine too (to omit the
withMutableSnapshot
)
e

escodro

02/18/2021, 6:13 PM
I don’t know if I’m following, but basically observe both directly is the way to go?
a

Adam Powell

02/18/2021, 6:17 PM
yes, and skip the
StateFlow
and go directly to
by mutableStateOf
object properties because the
StateFlow
isn't buying you anything here unless it's coming from a
.stateIn
from other cold flows. If you're using
MutableStateFlow
you're adding unnecessary complexity and steps, since
.collectAsState()
creates a
mutableStateOf
and writes into it using a `LaunchedEffect`/`.collect` anyway.
e

escodro

02/18/2021, 6:19 PM
Oh, I see. Didn’t know about that complexity for
StateFlow
as well. Thanks a lot! 😊
One last point: About the recomposition when observing multiple states from ViewModel(s). Is that okay? I was trying initially to have a SSOT to avoid this (combining states, for example).
a

Adam Powell

02/18/2021, 6:22 PM
If you write
Copy code
class MyState(private val dep: Dependency) {
  var foo by mutableStateOf(...)
    private set

  val bar: Bar
    get() = dep.bar

  fun updateFoo(...) {
    foo = ...
  }
}
then using it this way:
Copy code
val myState = remember(dep) { MyState(dep) }

Button(onClick = { myState.updateFoo(...) } {
  Text("Click to update")
}
Image(imageFor(myState.bar)), myState.bar.description)
then anything that is backed
by mutableStateOf
under the hood is observable, and when any of it changes the right thing will recompose. You can use this to maintain single sources of truth, and other objects' functions and properties can simply reference those sources of truth naturally.
The "combine" operations are implicit, you just use what you need.
No copies, no additional layers of caching values.
e

escodro

02/18/2021, 6:27 PM
Again, thanks a lot, Adam! I think that I need to study and practice more to understand it better. I also think that this is a great topic to cover in future artcicles/codelabs. 😊
👍 1
a

Adam Powell

02/18/2021, 6:33 PM
I totally agree! We've had a lot of discussion among the team around how to position teaching materials for audiences that are deep in existing android codebases with arch components+viewmodel+livedata, rxjava, flow, etc. vs. materials for green-field compose code
❤️ 3
we've chosen to focus on incremental adoption use cases first, but the tradeoff there is that interop cases involve a lot of existing constructs that don't need to be there for green-field compose code, and there's a risk of creating cargo cults by implying that these things need to be there for things to work, rather than that they're there for familiarity and smooth integration with existing code that already looks that way for other reasons.
This is also why things like the
snapshotFlow {}
API exist; it's an easy way to observe snapshot-powered objects written in the style I described above and consume it from other parts of your app that already speak Flow
e

escodro

02/18/2021, 6:40 PM
I can comment as a developer who never had any experience with other declarative UI (Flutter, ReactNative etc): sometimes is very difficult to know how to proceed with Compose because there a lot of interop things on the way. Things like: “Should we use ViewModel in a new Compose project?“, “Should I use LiveData, StateFlow or State?” and so on.
a

Adam Powell

02/18/2021, 6:45 PM
yeah, there are a lot of training wheels in these examples 🙂
😄 1
2 Views