Ryan Smith
11/28/2022, 11:33 PMMainScope()
from my application
block and pass it around
• My ViewModel instances receive the application CoroutineScope
as a constructor parameter and setup a view model scope from that using CoroutineScope(coroutineScope.coroutineContext + SupervisorJob())
• My ViewModel instances launch coroutines to the data layer using scope.launch
or scope.async
builders.
• At application shutdown, the application coroutine scope has it's cancel
method called like appCoroutineScope.cancel("Application Shutdown")
I originally thought this was sufficient to make sure all jobs were correctly cancelled on shutdown, but I haven't been able to convince myself things are actually shutting down properly.Arkadii Ivanov
11/29/2022, 12:14 AMRyan Smith
11/29/2022, 12:17 AMopen class ViewModel(coroutineScope: CoroutineScope) {
protected val job = SupervisorJob()
protected val scope: CoroutineScope = CoroutineScope(coroutineScope.coroutineContext + job)
}
Is creating a new scope within the application scope like this not enough to link them together?Casey Brooks
11/29/2022, 1:11 AMrememberCoroutineScope()
. The ViewModel would live as long as that function is in scope, which is probably what you want for a UI ViewModel.
Any work that lives longer than a Composable function would then need to be managed on a scope that lives outside of the Compose world, most likely. So launch those jobs onto MainScope()
, or create custom scopes as-needed per application logicRyan Smith
11/29/2022, 1:25 AMrememberCoroutineScope
since I already have trouble keeping context and scope straight in terms of when to use which since it's named rememberCoroutineScope
but takes a function that produces a CoroutineContext
lolCasey Brooks
11/29/2022, 1:30 AMCoroutineScope
is made up of CoroutineContext
elements, and you don’t need to worry about it much beyond that for typical usage. So don’t get too caught up on the actual signature of that function, it’s meant to be used like this:
class Screen1ViewModel(private val viewModelScope: CoroutineScope) {
fun doWork() = viewModelScope.launch {
// do a thing
}
}
@Composable
fun Screen1() {
val scope = rememberCoroutineScope { Dispatchers.Default }
val viewModel = remember(scope) { Screen1ViewModel(scope) }
}
Albert Chang
11/29/2022, 1:37 AMcoroutineScope.coroutineContext
?
When you cancel a scope, what is cancelled is its job, but since you are using a new job for each view model scope, I don’t think those scopes will be cancelled when you cancel the application scope.Ryan Smith
11/29/2022, 1:37 AM+--------------- Context -----------------+
| |
| +------ Scope ---------+ |
| | + launch | |
| | + async | |
| | + etc... | |
| | | |
| | | |
| | | |
| | | |
| +----------------------+ |
| |
| |
+-----------------------------------------+
Ryan Smith
11/29/2022, 1:38 AMRyan Smith
11/29/2022, 1:39 AMApplication Scope
|- ViewModel1 Scope
|- ViewModel1 Job 1
|- ViewModel1 Job 2
|- ViewModel1 Job 3
...
|- ViewModel2 Scope
|- ViewModel2 Job 1
|- ViewModel2 Job 2
|- ViewModel2 Job 3
...
Ryan Smith
11/29/2022, 1:40 AMapplicationScope.cancel
would propagate cancellation down to its whole treeAlbert Chang
11/29/2022, 1:44 AMRyan Smith
11/29/2022, 1:45 AMCoroutineScope(coroutineScope.coroutineContext + job)
would essentially add the view model's job as a child of the application scope jobAlbert Chang
11/29/2022, 1:46 AMRyan Smith
11/29/2022, 1:46 AMCasey Brooks
11/29/2022, 3:31 AMrememberCoroutineScope()
, I think things will generally work as you expect without too much fuss. Each point that you call rememberCoroutineScope()
will be cancelled if that point in the Compose hierarchy gets removed, so effectively it gives you a way to run your coroutines as long as that portion of the Compose UI hierarchy is visible. And the same will hold true in a multi-window application using the application { }
builder, where anything launched from the scope of the application root will live beyond any single window { }
, but anything launched within the window
will only be active if the window itself is open.
fun main() = application {
val applicationCoroutineScope = rememberCoroutineScope()
val applicationViewModel = remember(applicationCoroutineScope) { ApplicationViewModel(applicationCoroutineScope) }
window {
val windowCoroutineScope = rememberCoroutineScope()
val windowViewModel = remember(windowCoroutineScope) { WindowViewModel(windowCoroutineScope) }
val showScreen1 by windowViewModel.showScreen1.collectAsState()
if(showScreen1) {
screen1 {
// if `showScreen1` becomes false, this block will go away and `screen1CoroutineScope` will be
// cancelled, along with anything launched into its scope
val screen1CoroutineScope = rememberCoroutineScope()
val screen1ViewModel = remember(screen1CoroutineScope) { Screen1ViewModel(screen1CoroutineScope) }
}
}
screen2 {
// if `showScreen1` becomes false, this block will continue to stick around and its jobs will continue running
val screen2CoroutineScope = rememberCoroutineScope()
val screen2ViewModel = remember(screen2CoroutineScope) { Screen1ViewModel(screen2CoroutineScope) }
}
}
}
Michael Paus
11/29/2022, 12:07 PMCasey Brooks
11/29/2022, 3:06 PMremember { }
block should be set as one of its keysMichael Paus
11/29/2022, 4:26 PMCasey Brooks
11/29/2022, 4:32 PMCasey Brooks
11/29/2022, 4:34 PMMichael Paus
11/29/2022, 7:10 PM