b

    Bradleycorn

    1 year ago
    Is there something equivalent to
    remember()
    but that launches its
    calculation
    in a coroutine scoped to the composable? Something like:
    val myVal by remember(viewModel) {
        viewModel.someSuspendingMethod()
    }
    My use case is that I have a
    suspend
    method in a ViewModel who’s return value I need to
    remember
    . Sure I could make the ViewModel method a “normal” method and use
    viewModelScope
    to launch a coroutine, but that is going to be scoped to the ViewModel, which itself is scoped to the Activity. I want it to be scoped to the composable (so that if the composable is removed from the tree, the coroutine is canceled). I guess I could use
    produceState
    for this? Is that the best way?
    val myVal by produceState(initialValue = "Loading...", subject = viewModel) {
       value = viewModel.someSuspendingMethod()
    }
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    LaunchedEffect
    b

    Bradleycorn

    1 year ago
    yes … I thought about that too. But how do you get a returned value from
    LaunchedEffect
    ?
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    you could also use
    produceState
    like you said, i should have read your whole message 🙈
    that’s probably the best way
    all
    produceState
    does under the hood is combine
    LaunchedEffect
    with a
    MutableState
    basically
    b

    Bradleycorn

    1 year ago
    In this case, the suspending viewmodel method fetches some network data, updates the local database, and then returns the data from the database. I could certainly split that up into 2 methods, one to fetch data and update the database and use
    LaunchedEffect
    , and another non-suspending method with
    remember
    to get data from the database with a Flow. I actually had it like that at first …
    LaunchedEffect(viewModel) {
        viewModel.updateData()
    }
    
    val myVal by remember(viewModel) { 
        viewModel.getDbData() 
    }
    But the
    produceState
    solution seems more concise and readable to me, and like you said, it’s essentially doing the same thing under the hood.
    Anyway, thanks for the advice. compose coroutines compose + coroutines 🤯 My old man brain doesn’t absorb knowledge like it used to.
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    Actually, if
    updateData
    is a side effect, you shouldn’t call it inside a
    LaunchedEffect
    . You should use
    rememberCoroutineScope
    to get a scope scoped to the composition, and then call
    scope.launch{}
    to kick off the side effect (e.g. from an event handler or something)
    b

    Bradleycorn

    1 year ago
    hmm ok. So, that’s what I was really doing originally. But then I started reading the docs on
    rememberCoroutineScope
    and I came across this bit:
    Use this scope to launch jobs in response to callback events such as clicks or other user interaction where the response to that event needs to unfold over time and be cancelled if the composable managing that process leaves the composition. Jobs should never be launched into any coroutine scope as a side effect of composition itself. For scoped ongoing jobs initiated by composition, see [LaunchedEffect].
    I figured, well I’m not doing this in response to some callback or event. And I AM wanting a “scoped ongoing job initiated by composition”, so I went down the path of
    LaunchedEffect
    … And then in the end I wound up going to
    produceState
    cause at the end of it all, I want a
    State
    value to work with.
    This is where I get a little bit confused regarding Composables being free of side effects and idempotence. I get the idea. And the
    updateData
    method that gets called could certain be considered a side effect, as it fetches data from a network and updates a local database … But that work has to be triggered from somewhere … I could hoist it all the way up into the activity and out of the compose tree all-together, but then I’m loading the data every time the activity launches, whether the user ever ends up needing/wanting/viewing it or not. That’s not right. So, I’ve ended up with architecture in which i have a “Screen” Composable. It uses
    produceState
    (or
    LaunchedEffect
    or
    rememberCoroutineScope
    ) to do that work, and then call a “Content” composable that IS idempotent and pass it the data when it’s available.
    @Composable fun MyScreen(viewModel: ViewModel) {
        val myData = produceState(initalValue = .., viewModel) { ... } 
    
        when (myData)  {
           is Loading -> LoadingComposable()
           is Error -> ErrorComposable()
           else -> MyContentComposable(myData.data)
        }
    }
    Zach Klippenstein (he/him) [MOD]

    Zach Klippenstein (he/him) [MOD]

    1 year ago
    i mean, if the “event” is the initial composition of your composable, then
    LaunchedEffect
    makes sense
    b

    Bradleycorn

    1 year ago
    yeah
    Sean McQuillan [G]

    Sean McQuillan [G]

    1 year ago
    Yea – this is sideways data loading, which always kind feels a bit off. However, it ends up happening on screen level composables often (though you can put some indirection around it and send an "onScreenEntered" event, it nets the same). There's a few pieces moving here: 1. produceState starts a coroutine when called (either due to the initial composition event, or a change in viewModel) 2. The suspend functions execute (in the nomenclature of the state in compose page, this is an event handler) 3. The suspend function produces a value, and the event handler updates state (the return value of produceState) So – using produceState here lets you do sideways data loading and execute reasonably in this situation.
    Your instinct to consider hoisting it somewhere is spot on (though sometimes there's no better place to hoist it, then that means you've found a good spot for it). Good idea isolating it to a single composable near the top of the tree and then pass the result down. If you start doing it everywhere it can become a mess 😃.
    Re: side effects – none of this is a side effect of recomposition due to placing the code inside of
    produceState
    The big thing to avoid is something like this:
    @Composable
    fun MyComposable() {
        val scope = rememberCoroutineScope()
        if (somethingIsTrue) {
            scope.launch { whoKnowsHowManyTimes() }
        }
    }
    b

    Bradleycorn

    1 year ago
    Thanks @Sean McQuillan [G], that makes sense.
    Between you and @Zach Klippenstein (he/him) [MOD] I feel more comfortable that I’m on the right path, even if I’m not all the way there yet.