If I have some globally available `State` that is...
# compose-desktop
c
If I have some globally available
State
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.
a
It sounds like this question is conflating two different concerns: the scoping/ownership of the modification and what thread(s) it happens on
c
Ok, assuming that no state needs to be modified, I’d like to run an expensive operation off of the main thread. We can assume that the application doesn’t care about the result of this operation . Currently I’m using
GlobalScope.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?
z
Probably something like this:
Copy code
@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.
2
c
Rather than trying to synchronize a "global variable", it might be better to manage that data in a persistent data store, and use reactive APIs in the disparate portions of the application/UI to observe that store. For example, using SqlDelight and caching that data in a local sqlite DB, and observing queries as a Flow from the UI while the application updates the DB in the background
c
thanks so much for the feedback Zach!
Casey, I’m currently using the State wrappers along with the
derivedState {}
, 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 change
this understanding could be completely incorrect though and if so I’d love to know 😛
d
not saying it's best practice, but you can also do
Copy code
val workerThreadScope = rememberCoroutineScope { Dispatchers.Default }

workerThreadScope.launch { ... }
c
out of curiosity, why would that not be good practice?
d
it isn't, necessarily, i'm just not confident enough to say it is 😛
in Zach's example, he has the work itself dictate the dispatcher to use, whereas in mine, the scope dictates the (default) dispatcher
c
Whether you're using
rememberCoroutineScope { }
,
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 on
👍 1
All that said, the danger here is that you're trying long-running work to the UI at all. When you change screens and the composition that started the work leaves, the coroutine will be cancelled too. Or if you use
GlobalScope
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)