Whats the preferred method of adding a CoroutineScope to a Viewmodel in Desktop code? In Android we ...
a
Whats the preferred method of adding a CoroutineScope to a Viewmodel in Desktop code? In Android we have the lifecycle aware ones. I want to instantiate the ViewModel via Koin or KodeinDI in the Composable. But how do I pass a Composable scoped CoroutineScope which will get cancelled when the Composable is removed?
h
There is
rememberCoroutineScope
getting canceled when the composable is removed: https://developer.android.com/jetpack/compose/side-effects#remembercoroutinescope
a
True, I figured that one is needed. But I am thinking of how I can get access to it via a DI framework when instantiating it inside a Composable. Does that make sense?
h
I don't get it. What is the use-case for a DI framwork when getting the scope from
rememberCoroutineScope
? For testing you should provide a scope parameter.
c
To pass a value from the UI into DI, you need some form of “assisted injection”. You’re not meant to store the
rememberCoroutineScope
or other values like that within the DI graph itself, but it’s usually pretty well supported to pass those values into the DI framework at the point when you would get an instance of an object
m
I hope this is not complete nonsense but it works 😉. This is a typical fragment of my multiplatform Compose code.
Copy code
@Composable
fun DetailsScreen(talkId: String, onNavSelection: (target: ScreenSelector) -> Unit, onBackSelection: (() -> Unit)?) {
    val coroutineScope = rememberCoroutineScope()
    val viewModel = remember { DetailsScreenViewModel(coroutineScope, talkId) }
    DisposableEffect(true) { onDispose { viewModel.dispose() } }
    val detailsScreenState by viewModel.detailsScreenState.collectAsState()

    ...

}
I normally use Koin too but at this point I think that a normal constructor is just good enough. The view model takes an additional parameter
talkId
here which configures the specific view model instance. The view model then provides its state via a StateFlow which I import in the view via
collectAsState
. I am wondering though whether the
dispose
couldn’t be handled completely inside the view model based on the cancelling of the coroutine scope.
c
If everything’s fully in compose, this is what I typically do, too. The only difference is that I make sure everything is running on the VM’s
coroutineScope
, which gets cancelled automatically do I don’t need the
DisposableEffect
. Heres and example of the library I maintain and use for all my MPP ViewModels, doing this same pattern that Michael posted
m
How do you handle closing files or database connections in your view model when the coroutine context is cancelled?
c
Generally-speaking by using coroutine’s mechanisms like
callbackFlow { awaitClose { } }
. The Ballast library helps with observing flows and making sure everything’s done on the coroutineScope properly
There’s also
coroutineContext.job.invokeOnCompletion {  }
for use-cases that aren’t using Flows
m
Ahh, I think the second one would fit into my context. Thank you.
c
Yeah, it’s easier to make sure those long-running jobs are cleaned up if that cleanup code is close to where it is launched, rather than having to manually manage references to those connections at the ViewModel level and centralize the cleanup in a
dispose()
method.
a
@hfhbd the usecase is exactly the same as I see in other Android projects, like here: https://github.com/chrisbanes/tivi/blob/ccccd16a3cd18bc1d751df89de7c1cadf2d22791/ui/discover/src/main/java/app/tivi/home/discover/Discover.kt#L97 The hiltViewModel injects a ViewModel which contains a coroutine scope. But that one is lifecycle aware due to the lifecycles awareness of ViewModel. I want to recreate this in Desktop with Koin or Kodein DI
c
The
viewModelScope
and lifecycle part of the ViewModel class on Android is only necessary on Android. Desktop apps cannot use Android dependencies, but they also do not have the same kind of lifecycle issues that Android apps have. Furthermore, the
viewModelScope
is created by the Android framework and isn’t related to the UI, so those viewModels use a different “lifecycle” than the Composable functions they’re created in. For desktop apps, a normal class with a coroutineScope passed in to its constructor from
rememberCoroutineScope()
is the correct alternative. Here’s an example of the same basic idea being done with manual DI in a desktop app. If using Koin or Kodein, the only thing that changes is replacing the
ComposeDesktopInjector
with your Koin or Kodein instance
a
Thanks! That looks great. I’ll take a look at that
a
arks
684 Views