Cody Mikol
11/30/2021, 3:01 PMState
that is relevant to all screens in my application and I want to modify this via an expensive operation, is this the appropriate time to use GlobalScope.launch { … }
? I tried using remembeCoroutineScope
to launch the operation, but this caused my UI to freeze until the operation was finished.Adam Powell
11/30/2021, 3:04 PMCody Mikol
11/30/2021, 3:26 PMGlobalScope.launch { … }
, but I know that this is discouraged and seems to be for operations running over the entire lifetime of the application. The onEvent hooks for the built in buttons are blocking calls so I can’t simply launch { … }
a new coroutine. That is where I found the rememberCoroutineScope
functionality, but that also seems to block / freeze the UI. What is the appropriate way to launch some worker to do this expensive operation?Zach Klippenstein (he/him) [MOD]
11/30/2021, 4:56 PM@Composable fun YourComposable() {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch { longRunningWork() }
})
}
suspend fun longRunningWork() {
withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
blockingCall()
}
}
In general, suspend functions should never block the caller and always be responsible for getting themselves to a different thread if they need to block. A function being marked as suspend
should be an indication that it won’t block for long periods of time.Casey Brooks
11/30/2021, 5:35 PMCody Mikol
11/30/2021, 10:17 PMCody Mikol
11/30/2021, 10:20 PMderivedState {}
, I’m not super clear on how these things work behind the scenes, but my understanding is that when these are used to populate the ui, that component will hook into that reactive State
object and listen for changes, re-rendering that specific Composable
on a state changeCody Mikol
11/30/2021, 10:20 PMDavid W
11/30/2021, 10:35 PMval workerThreadScope = rememberCoroutineScope { Dispatchers.Default }
workerThreadScope.launch { ... }
Cody Mikol
11/30/2021, 10:36 PMDavid W
11/30/2021, 10:37 PMDavid W
11/30/2021, 10:37 PMCasey Brooks
11/30/2021, 10:52 PMrememberCoroutineScope { }
, launch() { }
or withContext()
to set the dispatcher, it's all effectively doing the same thing: combining the current coroutine context with a new one using the CoroutineContext.plus
operator. The main thing to know is that the default Compose coroutine context uses a Main
dispatcher, which is single-threaded, so any work launched in that coroutineScope will block the UI rendering. But as long as you're using rememberCoroutineScope
to access a coroutineScope to launch the background work, it will still be tied to the Composition, and be cancelled when the composition leaves. This is true regardless of which dispatcher you're running your long-running task onCasey Brooks
11/30/2021, 11:03 PMGlobalScope
you avoid that problem, then you've opened your application up to leaks, where you can't easily cancel that work if you need to. Not to mention that you've effectively made your Composable function non-pure by accessing a variable that is not passed directly to the Composable function (either by function parameters or CompositionLocals), which has its own set of problems. Compose generally assumed all Composable functions are pure, and things can go bad in subtle and hard-to-debug ways when you break that assumption.
This is why that long-running work should be moved outside of the Compose world entirely, into something like a ViewModel that lives outside of the UI (note that a ViewModel is more of a design pattern than a specific Android class)