I’m using `LaunchedEffect` along with `LocalLifecy...
# compose
b
I’m using
LaunchedEffect
along with
LocalLifecycleOwner
in a “Screen” composable to start a long-running/intensive coroutine in my ViewModel only when the containing Activity/Fragment is
RESUMED
. More info and code in the 🧵 I’m curious if this is a proper/best solution for my use case or if there are better ways to do this?
Code:
Copy code
@Composable
fun MyScreen(viewModel: MyScreenViewModel) {
    val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(key1 = viewModel) {
        lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
            viewModel.pollData()
        }
    }

    MyScreenContent(viewModel.uiState)
}
In my case,
viewModel.pollData()
is a suspend function that polls a network API every 30 seconds to get data updates. They get put into a Room DB, and updates to the DB trigger updates to the ViewModel’s
uiState
. Because it’s doing network polling very often, I only want the polling to run while the user actually using the app, so that I’m not needlessly draining the user’s battery and data plan while the app is backgrounded, or otherwise not being used. When this screen was built using the old View system, I would launch a coroutine for the polling whenever the Fragment was in the RESUMED state, and cancel it in the PAUSED state. That ensures that I am only polling when absolutely necessary. The above code is how I implemented similar functionality with compose.
t
A lot of the best practices around this seems to be around decoupling your ViewModels and your Composables (I guess this goes for screen Composables as well): https://kotlinlang.slack.com/archives/CJLTWPH7S/p1618598956479500?thread_ts=1618598414.479200&cid=CJLTWPH7S
another option would be to keep
pollData()
as part of a separate implementation of
LifecycleObserver
that you just attach to the host Activity/Fragment, and leave it out of the
ViewModel
/ rendering layers
b
Sure. No reason the composable above couldn’t adhere to both things mentioned by @jim. The
MyScreenViewModel
could definitely be an interface, and it already does the 2nd thing. The logic for the polling is encapsulated in a very top level composable for this Screen, and that composable only does only this one thing and then calls the “Content” composable which only receives state that it needs to draw itself and no view model at all. I agree that in general, decoupling the ViewModel and the composables that render content as much as possible is a great idea. But at very high levels (such as the composable that is called from the NavigationGraph when a particular route is given to the NavController and is the root of an entire Screen), there is a coupling of the ViewModel, which provides the state that is then handed down the tree to child composables that render the ui that makes up the screen. My question is more abstract really than ViewModels … First, I have some producer that produces state that is passed to a composable, and that producer is long running and resource intensive, so I want to make sure that it only runs while the composable is in the composition.
LaunchedEffect
gives me that. I think that is the best way to handle that part of the problem, but I’m curious if there are other/better ways. Second, there are times when the composable is in the composition (so the LaunchedEffect is NOT canceled), but is not visible and as such, the producer should not be running. An example is when an app is put into the background. So, to handle that, I use the
LifeCycleOwner.current
to cancel/restart the producer when the Lifecycle exits/re-enters the RESUMED state. That works for that part of the problem, but I’m curious if there are other ways to do that as well.
t
You could, in the view model/state object, expose a
Flow
that suspends while the lifecycle is not resumed, and collect that in the Composable with
Flow.collectAsState
. But I prefer LaunchedEffect myself.
a
In general the lifecycle should be kept out of the viewmodel for the architecture to feature clear separations. With your LaunchedEffect construction you are scoping the operations with two things: 1) Scoped to the composition via LaunchedEffect 2) Scoped to the Lifecycle between Resumed and Paused via repeatOnLifecycle This seems like a well build architecture to me.
You might want to add lifecycleowner as second key for the LaunchedEffect though!
☝🏼 1
256 Views