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

Bradleycorn

12/04/2020, 7:16 PM
Is there something equivalent to
remember()
but that launches its
calculation
in a coroutine scoped to the composable? Something like:
Copy code
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?
Copy code
val myVal by produceState(initialValue = "Loading...", subject = viewModel) {
   value = viewModel.someSuspendingMethod()
}
z

Zach Klippenstein (he/him) [MOD]

12/04/2020, 7:16 PM
LaunchedEffect
b

Bradleycorn

12/04/2020, 7:18 PM
yes … I thought about that too. But how do you get a returned value from
LaunchedEffect
?
z

Zach Klippenstein (he/him) [MOD]

12/04/2020, 7:24 PM
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

12/04/2020, 7:26 PM
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 …
Copy code
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.
z

Zach Klippenstein (he/him) [MOD]

12/04/2020, 7:38 PM
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

12/04/2020, 7:43 PM
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.
👍 1
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.
Copy code
@Composable fun MyScreen(viewModel: ViewModel) {
    val myData = produceState(initalValue = .., viewModel) { ... } 

    when (myData)  {
       is Loading -> LoadingComposable()
       is Error -> ErrorComposable()
       else -> MyContentComposable(myData.data)
    }
}
z

Zach Klippenstein (he/him) [MOD]

12/04/2020, 8:17 PM
i mean, if the “event” is the initial composition of your composable, then
LaunchedEffect
makes sense
b

Bradleycorn

12/04/2020, 8:18 PM
yeah
s

Sean McQuillan [G]

12/05/2020, 12:30 AM
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:
Copy code
@Composable
fun MyComposable() {
    val scope = rememberCoroutineScope()
    if (somethingIsTrue) {
        scope.launch { whoKnowsHowManyTimes() }
    }
}
b

Bradleycorn

12/05/2020, 12:38 AM
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.
👍 1
8 Views