Thread
#compose
    b

    Bradleycorn

    10 months ago
    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:
    @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.
    Tash

    Tash

    10 months ago
    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

    Bradleycorn

    10 months ago
    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

    tad

    10 months ago
    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.
    Alex

    Alex

    10 months ago
    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!