Is it possible to get an `immediate` version of th...
# compose-desktop
j
Is it possible to get an
immediate
version of the compose
CoroutineDispatcher
in common code?
Dispatchers.Main
throws an exception for desktop and it looks like the dispatcher used is actual FlushCoroutineDispatcher, which doesn't implement
immediate
and is
internal
.
I'm looking for a clean way to share view model state between Compose UI and Swift UI on iOS. `StateFlow`s seem the most promising, but certain UI components, like
TextField
don't work well with asynchronous data sources. So an
immediate
CoroutineDispatcher
is needed in order to make `StateFlow`s work.
a
It should work. You can try adding the corresponding dependency, e.g. kotlinx-coroutines-swing. See https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/README.md
j
By adding the dependency, does Compose start using it for desktop? I saw this change and figured it doesn't use the Swing dispatcher now.
a
I think that PR removes the direct dependency on Swing dispatcher, allowing clients to specify it instead. See: https://github.com/JetBrains/compose-multiplatform/releases/tag/v1.1.1
I'm surprised I couldn't find any docs about it.
m
I believe they did it so that so that compose can be used in IntelliJ plugins, the coroutine main dispatcher in Intellij isn't the Swing one
k
oh wow, nice find, ill try it out as im encountering the same issue
j
I added the
org.jetbrains.kotlinx:kotlinx-coroutines-swing
dependency and found Compose still uses the internal FlushCoroutineDispatcher. I can't find an API to provide a different
CoroutineDispatcher
to use. The way I read those release notes:
Also, usage of
Dispatchers.Swing
or
Dispatchers.Main
inside internals of Compose is implementation details, and can be changed in the future.
it seems that the dispatcher was later changed and is considered an implementation detail.
It would be nice if the dispatcher used in the Compose scope was an immediate
CoroutineDispatcher
by default, so coroutines launched in the scope and flows collected would behave synchronously when possible. This is the case for viewModel and lifecycle scopes on Android.
I created this issue.
After further testing, it does look like using
Dispatchers.Main.immediate
from the
kotlinx-coroutines-swing
dependency works to synchronously launch a coroutine on the same thread as the Compose
CoroutineScope
. Even though it's a different
CoroutineDispatcher
implementation, they appear to use the same thread. I'm not sure if this is a guarantee Compose desktop provides or considered an implementation detail.
a
It would be nice if the dispatcher used in the Compose scope was an immediate
Yes! I would also love to have this. But this looks unrelated to the issue of the missing or incorrect Main dispatcher in Compose for Desktop.
I guess the request for the Main.immediate dispatcher should go to https://issuetracker.google.com/issues/new?component=610764&template=1424126 Worth asking in #compose .
I think the Main dispatcher on JVM is discovered using the Java ServiceLoader mechanism, see https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/src/internal/MainDispatchers.kt#L17.
j
A workaround is executing
System.setProperty("kotlinx.coroutines.fast.service.loader", "false")
before your compose code It seems to be a problem related to how the runtime is creating the dispatchers as soon as the app is launched
a
Google are fixing the issue with
TextField
and
StateFlow
in
TextField2
.
Also you can provide your own coroutine context to
rememberCoroutineScope
.
Copy code
fun main() = singleWindowApplication {
    Column {
        var textFieldValue by remember { mutableStateOf(TextFieldValue()) }
        TextField(
            value = textFieldValue,
            onValueChange = { textFieldValue = it }
        )

        val coroutineScope = rememberCoroutineScope(
            getContext = { Dispatchers.Unconfined }
        )

        Button(
            onClick = {
                println("Before launch")
                coroutineScope.launch {
                    println("In coroutine")
                }
                println("After launch")
            }
        ) {
            Text("Click me")
        }
    }
}
a
Indeed, but it's still very nice to be able to update the UI state on the same call stack as the UI event. I think it should be helpful in cases like when we want to disable a button on click, to prevent double clicks, etc. Re-dispatching state updates have been causing issues for me since the beginning.
a
Why can’t you already update the UI state on the same call stack as the UI event? Can you share an example?
a
When using StateFlow with collectAState, the latter uses Dispatchers.Main by default (at least I remember this was the case), which re-dispatches all state updates. I've been specifying Main.immediate explicitly all the time to prevent this. It fixes the issue with TextField, and also some other issues as well like double clicks after disabling the button.
a
Can you give an example? How do you solve the button issue with
Dispatchers.Main.immediate
?
a
Sure! From the top of my head.
👆🏼 1
a
I don’t think that solves the problem completely. I mean it solves the problem with StateFlow getting updated quickly, but you can still get two clicks.
If you have two clicks with no recomposition between them.
So you need to also check the state in onButtonClicked
a
Well, if it doesn't then this is pretty sad. Though, I never had any issues with the immediate dispatcher, but I had plenty without it. As in comparison with normal Android Views, if I set 'isEnabled = false` to any view, then click listeners will stop working immediately. For some reason, I would expect the Button widget to recompose before it processes the next click, as the state change is placed into the "queue" before the click.
I would love to have this kind of guarantees in Compose, but this is a separate topic I guess.
a
No, it’s the same on Android
a
I believe no. If I call setEnabled(false), then it sets the boolean flag to false, and then any click dispatching in the view checks the flag internally before calling the listener.
a
It’s possible that Android itself doesn’t dispatch clicks that often, or you just can’t tap that quickly with your finger, but you can reproduce it with a test.
If there’s no recomposition between the two clicks, the button just doesn’t know that anything changed
Input events are dispatched immediately as they occur. Recomposition only happens at most once every frame.
a
Input events are dispatched immediately as they occur. Recomposition only happens at most once every frame.
Yeah, this makes sense. Seems like a trade-off.
I believe the Android View class even cancels any pending input events when the view is disabled. So I'm 100% sure that no listener will be called after setEnabled(false) returned.
Maybe it would be a good idea to change
enabled: Boolean
to something like
enabled: () -> Boolean
? I believe we have a similar API in
HorizontalPager
, its
PagerState
has
pageCount: () -> Int
, perhaps for similar purposes.
a
As in comparison with normal Android Views, if I set ’isEnabled = false` to any view, then click listeners will stop working immediately.
Ah, I misread that. I meant that it’s the same in Jetpack Compose on Android. Native Android views behave differently, of course.
a
Thanks for the clarification! Yeah, I think it should be the same.
Also, I think one can create the following "fixed" button, which should work fine with
Dispatchers.Main.immediate
but not with
Dispatchers.Main
. It still looks pretty important to update the State synchronously, so that we can "see" the new state right after notifying the VM about the change.
💯 1
j
TextField2
addresses some of these issues specific to
TextField
, but these sort of asynchronous state update issues can affect many component use cases, as Arkadii pointed out. And a workaround like his last button example is only possible if the state can be synchronously read as soon as it's updated, which
Dispatchers.Main.immediate
provides for. Is there a reason Compose doesn't use an immediate dispatcher by default like Android's
ViewModel
and
Lifecycle
`CoroutineScope`s?
a
You’re more likely to receive a good answer in the #compose channel.
👍🏼 1
More Googlers there
e
I'm using
immediate
for a specific use case (but it's at the architecture level so it gets used everywhere). Would be nice to not have to do this (although my main use case for this is text updates, and that should go away with BTF2 as mentioned above) https://github.com/eygraber/vice/blob/master/vice-portal/src/commonMain/kotlin/com/eygraber/vice/portal/VicePortal.kt#L32