What is the recommended way to respond to flows th...
# compose
o
What is the recommended way to respond to flows that act like events/side effects in Compose? E.g. a viewmodel with a Flow<NavigateToFoo>
s
State flows down, so change it to
Flow<NavigationDestination>
o
How do I respond to this in my Composable?
1
s
It's state, so you read it val currentScreen = navFlow.collectAsState() // show current screen
Longer answer, if you cant transform an event source at the creation location (say it's deep in a subsystem), you should transform it to state by collecting the events it outside of compose and writing the current state to a state object for consumption in compose. In compose, you should be flowing state down.
o
It's not State, it's a viewmodel of Screen A, and it sends down an event that tells it to navigate to Screen B. Unfortunately I can't keep them as state right now (Im using navigation component, Screen B is still Android view)
So yes, ideally it should all be state and effects should be minimized, but right now I dont think thats possible for my case
s
You'll want to transform it to state then
o
You mean something like a Flow<PendingNavigationRequest> ? That then consumes the state
s
Well, say you have a
Flow<NavigateToFoo>
, you can collect that (unrelated to compose) and produce a
State<CurrentNavScreen>
. Ignore the first in compose and only use the second. (If you prefer, that can be a flow or any other state-like observable type)
o
I will show you my actual use case, it's not navigation. My viewmodel has the following flow: val loggableExceptions: Flow<Exception> Now I want to log this exception in Compose, but obviously it's a side effect, how should this be done?
s
Oh interesting
o
With logging, it's just Firebase.crashlytics.recordException(e)
s
This seems like it's not exactly a UI concern, so I'd suggest trying to remove it from Compose all together. However, if you want to launch a coroutine to have a side effect like this, you can use
Copy code
LaunchedEffect(sourceFlow) {
    sourceFlow.//operations
}
This will launch a coroutine once per composable instance (and restart it if sourceFlow changes).
👍 1
o
Thanks, I went with this:
Copy code
LaunchedEffect(Unit) {
    viewModel.nonFatalErrors
        .onEach { exception -> Firebase.crashlytics.recordException(exception) }
        .launchIn(this)
}
s
You should pass
viewModel.nonFatalErrors
as the key there, otherwise you can run into under-restart problems as the code changes if the surrounding code ever swaps the Flow instance (this is a very common mistake, and it takes a bit to get the hang of it)
Copy code
LaunchedEffect(viewModel.nonFatalErrors) {
    viewModel.nonFatalErrors
        .onEach { exception -> Firebase.crashlytics.recordException(exception) }
        .launchIn(this)
}
o
Could you elaborate? Shouldn't it be a non- problem since the Unit as key results in LaunchedEffect block only being run once?
s
Yes, it will only run once, which is not desired if instance of
Flow
changes on recomposition. In that case, you'd want to cancel the previous and restart a new collection. Some quick rules for keys: • All locals that are captured longer that the current recomposition lifetime should be keys • All locals that are referenced (time delayed) after the current recomposition scope should either be keys, or wrapped in
rememberUpdatedState
(rememberUpdatedState is useful for wrapping callback parameters in a box that allows them to be safely read later with the most recent value from the last composition)
The general category of bugs that arises here is called "over capture" (referencing a stale value of a callback lambda) or "under restart" (continuing to read from a flow that everyone else forgot about)
o
Hmm interesting
On a other note, let's say there is another flow that needs to be collected from the vm, is it better to put it in the same LaunchedEffect, or should there be another LaunchedEffects for it ?(Making it two in totall)
s
Generally, I'd recommend that Flow represents state-like things and is read via
Flow.collectAsState()
in compose. This is more of a special case exit valve.
o
I know, but for this case, it's actually a viewModel.callPhoneNumbereEvents flow
s
For code structure, you can do either. Or even define a l`logExceptions(Flow<Exception>)` and move this effect into there
o
It starts the Android intent of the system way of starting a audio call
(telecom call)
s
I'm hesitating cause there's a lot of options here and I'm not sure which is best, let me detail them 🙂
1. Move the side-effect to a Control object that exposes an imperative side-effect API (e.g.
.startPhoneCall
2. Pass enough information to the VM event handler to allow the event handler to handle the event entirely (this code belongs "in" an event handler) 3. Create an event and collect it in recomposition
#3 is my least favorite as it's really using recomposition to have side effects (even using LaunchedEffect, it's kinda fighting the system)
Ideally, the side-effect happens directly from the event handler without going through recomposition to be processed
o
What does event handler refer to?
s
The event that's triggering this phone call
o
You mean the callPhoneNumberEvents Flow?
s
Ah the more abstract event
o
The event itself just contains a phone number
s
i.e. the thing that caused all of this to be triggered. I assume a button press or something
o
Ah I see what you mean now, interesting
But I went for this way to let the viewmodel decide explicitly, in order to keep the view dumb
(View just lets vm know if button pressed or not, vm will tell you what to do)
s
(in general, events going down are something to design to avoid, especially in Compose - recomposition can be a surprising execution engine as you start using it in more complex ways due to skipping and other optimizations)
o
Does it produce different results than React's useEffect()?
s
They're pretty similar in design
cc @Adam Powell and @Leland Richardson [G] who wrote most of them 🙂
o
What does your first option refer to? What is a Control object?
s
A controller that wraps up the system API in a way that allows it to be reasonably shared with the (ViewModel?, event handler?)
o
And the viewmodel calls that Control object?
s
yea `The basic idea is that the event handler (the button onClick) should be calling
Or the
onClick
listener
Creating a state-event is really just adding indirection to the side-effect and introducing a recomposition latency to the call
o
What if the event is not getting triggered by a user interaction, but from outside
s
Same thing
o
Then there will be no onClick to put the code in
s
Well, in the place you would write the state-event, you can trigger the side-effect directly
o
I understand, but I'm hesitant in coupling the viewmodel with system level / framework API's
OFcourse I could still wrap it in a interface
s
You can decouple it in various ways. Interface, or even using the state-events in a Flow that we're talking about here to make a really light coupling from the VM to the controller
The reason you're hitting state-events going to composition is that you've not hoisted the event handler high enough
(in this case, to the ViewModel)
To share the general arch design I'm working through here 🙂