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

Orhan Tozan

03/02/2021, 7:28 PM
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

Sean McQuillan [G]

03/02/2021, 7:29 PM
State flows down, so change it to
Flow<NavigationDestination>
o

Orhan Tozan

03/02/2021, 7:30 PM
How do I respond to this in my Composable?
1
s

Sean McQuillan [G]

03/02/2021, 7:31 PM
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

Orhan Tozan

03/02/2021, 7:32 PM
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

Sean McQuillan [G]

03/02/2021, 7:33 PM
You'll want to transform it to state then
o

Orhan Tozan

03/02/2021, 7:34 PM
You mean something like a Flow<PendingNavigationRequest> ? That then consumes the state
s

Sean McQuillan [G]

03/02/2021, 7:36 PM
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

Orhan Tozan

03/02/2021, 7:36 PM
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

Sean McQuillan [G]

03/02/2021, 7:36 PM
Oh interesting
o

Orhan Tozan

03/02/2021, 7:37 PM
With logging, it's just Firebase.crashlytics.recordException(e)
s

Sean McQuillan [G]

03/02/2021, 7:39 PM
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

Orhan Tozan

03/02/2021, 7:45 PM
Thanks, I went with this:
Copy code
LaunchedEffect(Unit) {
    viewModel.nonFatalErrors
        .onEach { exception -> Firebase.crashlytics.recordException(exception) }
        .launchIn(this)
}
s

Sean McQuillan [G]

03/02/2021, 7:47 PM
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

Orhan Tozan

03/02/2021, 7:49 PM
Could you elaborate? Shouldn't it be a non- problem since the Unit as key results in LaunchedEffect block only being run once?
s

Sean McQuillan [G]

03/02/2021, 7:50 PM
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

Orhan Tozan

03/02/2021, 7:55 PM
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

Sean McQuillan [G]

03/02/2021, 7:58 PM
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

Orhan Tozan

03/02/2021, 7:58 PM
I know, but for this case, it's actually a viewModel.callPhoneNumbereEvents flow
s

Sean McQuillan [G]

03/02/2021, 7:59 PM
For code structure, you can do either. Or even define a l`logExceptions(Flow<Exception>)` and move this effect into there
o

Orhan Tozan

03/02/2021, 7:59 PM
It starts the Android intent of the system way of starting a audio call
(telecom call)
s

Sean McQuillan [G]

03/02/2021, 8:02 PM
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

Orhan Tozan

03/02/2021, 8:04 PM
What does event handler refer to?
s

Sean McQuillan [G]

03/02/2021, 8:05 PM
The event that's triggering this phone call
o

Orhan Tozan

03/02/2021, 8:05 PM
You mean the callPhoneNumberEvents Flow?
s

Sean McQuillan [G]

03/02/2021, 8:05 PM
Ah the more abstract event
o

Orhan Tozan

03/02/2021, 8:05 PM
The event itself just contains a phone number
s

Sean McQuillan [G]

03/02/2021, 8:06 PM
i.e. the thing that caused all of this to be triggered. I assume a button press or something
o

Orhan Tozan

03/02/2021, 8:06 PM
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

Sean McQuillan [G]

03/02/2021, 8:07 PM
(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

Orhan Tozan

03/02/2021, 8:08 PM
Does it produce different results than React's useEffect()?
s

Sean McQuillan [G]

03/02/2021, 8:08 PM
They're pretty similar in design
cc @Adam Powell and @Leland Richardson [G] who wrote most of them 🙂
o

Orhan Tozan

03/02/2021, 8:10 PM
What does your first option refer to? What is a Control object?
s

Sean McQuillan [G]

03/02/2021, 8:11 PM
A controller that wraps up the system API in a way that allows it to be reasonably shared with the (ViewModel?, event handler?)
o

Orhan Tozan

03/02/2021, 8:11 PM
And the viewmodel calls that Control object?
s

Sean McQuillan [G]

03/02/2021, 8:12 PM
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

Orhan Tozan

03/02/2021, 8:12 PM
What if the event is not getting triggered by a user interaction, but from outside
s

Sean McQuillan [G]

03/02/2021, 8:12 PM
Same thing
o

Orhan Tozan

03/02/2021, 8:13 PM
Then there will be no onClick to put the code in
s

Sean McQuillan [G]

03/02/2021, 8:14 PM
Well, in the place you would write the state-event, you can trigger the side-effect directly
o

Orhan Tozan

03/02/2021, 8:14 PM
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

Sean McQuillan [G]

03/02/2021, 8:15 PM
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 🙂