Got a question regarding safely consuming events t...
# compose
s
Got a question regarding safely consuming events that are not state from a Flow inside a @Composable context. More in thread 🧵
Got a ViewModel which exposes a flow like:
Copy code
private val eventChannel = Channel<Event>(Channel.UNLIMITED)
val eventsFlow = eventChannel.receiveAsFlow()
What is the “correct” way to collect it? Right now I am doing this:
Copy code
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(viewModel.eventsFlow, lifecycleOwner) {
    lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.eventsFlow.collect { event ->
            when (event) {
                is Event.Foo -> doSomethingWithFooEvent(event)
            }
        }
    }
}
Is there anything I am doing which is not necessary? key-ing the LaunchedEffect to the flow itself and the lifecycleOwner seems like it’s a must, and then I am slapping a repeatOnLifecycle in there too because I am guessing I don’t want to consume the event while the App isn’t started and showing on the screen. It certainly feels like quite a ceremony and 4 levels of indentation isn’t great either, but if this is the way to do this maybe we could make some helped functions to make this prettier. I’ve seen a lot of discussions about this, but all the ones I’ve seen revolve around consuming a flow and converting in to State, which isn’t something I want to do here, hence the new thread 🤗 Also I am aware of the new guidelines of having events as State, and consuming them and notifying the ViewModel that it was consumed, but we can’t migrate the entire code-base instantly, so we’ll have to make do with this pattern too for now.
c
Given that you're collecting the Flow from Compose, I'd think you wouldn't need to collect it within the context of the
LifecycleOwner
. The composition itself should be bound by the lifecycle, so simply collecting the flow within a normal
LaunchedEffect
should scope it to the composition and ensure it's only collected during a valid lifecycle (which is when the composition itself is alive)
s
If you need the flow as a state within your compose code then you can just do something like
Copy code
val myVal = remember { vm.eventsFlow }.collectAsState(DEFAULT_VALUE).value
if you want to handle it not as state, then you can do it simply in the
LaunchedEffect
Copy code
LaunchedEffect(Unit) {
    vm.eventsFlow.collect { do whatever with your data }
}
I don't think you need the
eventsFlow
nor the
lifecycleOwner
as keys, since neither of them will ever change
s
Okay I just tested in a sample project and I absolutely can not just do
Copy code
LaunchedEffect(Unit) {
    viewModel.eventsFlow.collect { event ->
       when (event) {
            is Event.Foo -> Timber.d(event)
        }
    }
}
Since when I am in the app, and I click the home button exiting the app I can see that it keeps collecting and the logs keep on coming even though I am in the home screen. This is exactly what I am avoiding by adding the repeatOnLifecycle line.
This could potentially be even dangerous depending on what I do with that data no? Since that will run after my app has left the screen and is now in the
CREATED
state, not in the
STARTED
state.
c
If your events flow can't be represented as a state and depends so fully on the Android lifecycle, then I'd say it shouldn't be part of Compose at all. Since the events are coming from a ViewModel, those events should stay within the lifecycle within the Fragment or Activity that created/host the ViewModel, rather than within Compose, since that's the relevant "lifecycle" that controls them
s
Let’s imagine this scenario though. I am in a compose-only app, and I am using compose navigation to scope the VM to my destination, therefore at the context I am in I don’t have a hold of the Fragment/Activity. At that point, I want to do whatever business logic inside my VM and at some cases I need to do something like let’s say start another App through an Intent. Wouldn’t that be a case where it can’t just lie inside the VM, it’s not state, and I can’t listen to that event while the app is in the background, but only while the user is actively using the app. What would you do in that scenario?
t
Currently struggling with exactly this @Stylianos Gakis, and I don't see another way out other than what you have because: • you need to collect events (not state), because navigating to another activity or app is not the same as "state" as understood by the Compose environment • you need to scope your collector to
Lifecycle
I've been using something like this from the Tivi project
s
Yeah I still don’t see another way either. The Tivi link seems like a great approach to this. I might start adopting it too if I don’t find any better alternatives anytime soon. Especially interesting how
collectAsState
also seems to have the same problem of collecting while the user has pressed the home button. From what I understand this isn’t normally a problem since it’s safe to update compose state while the app is in the background, but even if it’s safe it may be wasting resources for no reason since it may keep alive the flows it’s observing from for no reason.
t
Yeah, I remember this thread from a while ago (you were in it) https://kotlinlang.slack.com/archives/CJLTWPH7S/p1629467332137500?thread_ts=1629466726.134200&amp;cid=CJLTWPH7S But still felt a little hand-wavy, so created this issue https://issuetracker.google.com/issues/206864371 hoping for more "official guidance" so that we know we're on the right track
s
I know lmao, I was reading this post as well these days and I was like “Wait, I made this question?” 😂
😂 1
😭 1