Seems I have a redrawing problem. I have ```val va...
# compose-desktop
a
Seems I have a redrawing problem. I have
Copy code
val value by stateFlow.collectAsState()
println("Value: $value")
Text(
    text = value
)
and a new value is getting printed, but the old value is displayed until something else in the window is redrawn.
d
Maybe your value have type with custom equals and hashCode function? I think, better to use data classes for storing view state.
a
It’s a
Double
Well, to be exact, it’s a
Double
that is converted to
String
like this:
Copy code
val value by stateFlow.collectAsState()
println("Value: $value")
Text(
    text = "Value: ${value.roundToInt()}"
)
and the value changes such that its
roundToInt()
changes too
d
Double is a very interesting type. For example 1.0/3 + 1.0/3 + 1.0/3 is not euqals to 1.0 So equals with Double work's unpredictable in some cases.
a
I know, but I don’t think it’s relevant here
I’m getting prints of:
Copy code
Value: 647.3730582038996
Value: 647.3730582038996
Value: 664.5584672349289
and the UI shows 647
and when I move the mouse somewhere completely different which causes a mouse-over effect, the UI changes to 665
I’ll try to get a small reproduction
What’s special here is that the stateflow is a complex combination of several other flows whose values change rapidly together.
d
good, sample will help in investigation
Also, you may try this code:
Copy code
val value by stateFlow.collectAsState()
println("Value: $value")
val derivedValue by derivedStateOf { "Value: ${value.roundToInt()}" } 
Text(
  text = derivedValue
)
a
I’m still working on reducing it to pinpoint the exact cause, but here’s what I have so far:
Copy code
class Flows{
    
    private val v1 = MutableStateFlow(0.0)
    private val v2 = MutableStateFlow(0.0)

    private val valuesFlow = MutableStateFlow(
        listOf(v1.asStateFlow(), v2.asStateFlow()),
    ).asStateFlow()

    @OptIn(ExperimentalCoroutinesApi::class)
    val combination = valuesFlow.flatMapLatest { items ->
        combine(
            items,
            Array<Double>::sum
        )
    }

    val _generation = MutableStateFlow(0)
    val generation = _generation.asStateFlow()

    suspend fun next(){
        v1.value = v1.value + 1
        delay(1)
        v2.value = v2.value + 1
        _generation.value += 1
    }

    suspend fun prev(){
        v1.value = v1.value - 1
        delay(1)
        v2.value = v2.value - 1
        _generation.value -= 1
    }

}


@Composable
fun DisplayFlows(flows: Flows){
    val coroutineScope = rememberCoroutineScope()
    val value by remember(flows.combination){
        flows.combination.stateIn(
            scope = coroutineScope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = 0.0
        )
    }.collectAsState()

    println("Value: $value")
    Text(
        text = "Value: ${value.roundToInt()}"
    )
}


@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ModifyFlows(flows: Flows){
    val focusRequester = remember{ FocusRequester() }
    val coroutineScope = rememberCoroutineScope()
    Box(
        modifier = Modifier
            .size(120.dp, 100.dp)
            .background(Color.LightGray)
            .focusRequester(focusRequester)
            .focusable()
            .focusTarget()
            .onKeyEvent {
                if (it.type == KeyEventType.KeyUp){
                    if (it.key == Key.DirectionLeft){
                        coroutineScope.launch {
                            flows.prev()
                        }
                    }
                    else if (it.key == Key.DirectionRight){
                        coroutineScope.launch {
                            flows.next()
                        }
                    }
                }

                true
            }
    ){
        val generation by flows.generation.collectAsState()
        Text(
            text = "Press left or right\nGeneration: $generation",
            modifier = Modifier.align(Alignment.Center)
        )
    }

    LaunchedEffect(Unit){
        focusRequester.requestFocus()
    }
}


fun main() {
    singleWindowApplication(
        title = "Test",
        state = WindowState(
            width = 400.dp,
            height = 600.dp,
        )
    ) {
        val flows = remember{ Flows() }
        Column(
            modifier = Modifier.padding(16.dp).fillMaxWidth(),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            DisplayFlows(flows)
            ModifyFlows(flows)
            Button(
                onClick = {}
            ){
                Text("Move mouse over me\nto repaint value")
            }
        }
    }
}
Press right/left for a bit and you’ll see the value being odd, which it should never be. The last number printed is of course always even. Then move the mouse over the button, which has a mouse-over effect, and this will cause a repaint and the displayed value will be correct again.
Seems to be a very specific combination of things that causes this. If I do any of these changes, it doesn’t manifest: 1. Get rid of
flatMapLatest
2. Don’t show the generation 3. Remove the
delay(1)
d
Very interesting bug
Unfortunately it doesn't reproduced on my computer yet
a
If I had to guess, I’d say it’s probably some kind of race condition between code that marks regions as dirty and the repainting.
d
What version of Compose and Kotlin do you use?
a
Copy code
plugins {
    kotlin("jvm") version "1.6.10"
    id("org.jetbrains.compose") version "1.1.1"
}
With Compose 1.2.0-alpha01-dev716 it doesn’t grab the focus by itself 😕
Reproduces with
Copy code
plugins {
    kotlin("jvm") version "1.6.21"
    id("org.jetbrains.compose") version "1.2.0-alpha01-dev716"
}
for me
(after removing
focusTarget()
)
It seems to show odd values after 2-3 key presses, and then only shows even values.
Reproduces if the value type is
Int
as well.
d
I will ask my team to try reproduce this bug on their computers
What kind of Mac do you have?
Intel or M1 CPU?
a
Intel
message has been deleted
Reproduces with Oracle JRE 17, and 11.0.15
d
Can you please check and try to reproduce bug right in this repo: https://github.com/dima-avdeev-jb/reproduce-compose-desktop-bug If bug's reproduced, I will send this repo to my colleagues to check reproduction.
a
It reproduces
d
Cool, I also reproduced this sample on my old second Mac BigSur with Intel CPU. On M1 it not reproduced.
👍 1
r
I am also seeing this exact same issue. I have an Intel Mac. I haven't tested on Windows to see if it's an issue there yet.
d
On M1 it also reproduced, but with less chance.
r
Is there a workaround for this in the meantime? I only see this happening in one place in my app which is a pretty large application. I only see it happening in a Popup in my application.
d
I will research for workaround
r
I was unable to reproduce on windows in my limited testing.
a
Have a tiny animation running at all times somewhere in the window 😉
On a more serious note, I think this is such a delicate combination of things that causes it, that you’re pretty unlikely to see it in your app.
Although it's possible there are other ways to trigger it.
d
I have changed a little bit reproduction sample, and now it reproduces on the latest dev versions: I just comment .focusTarget(). It was redundant in this case. https://github.com/dima-avdeev-jb/reproduce-compose-desktop-bug/blob/main/reproduce-bug/src/jvmMain/kotlin/Main.kt#L89
a
This reproduces much easier on a busy computer. For example if you have the compose-jb/compose project open in IntelliJ 😉
btw, I’m seeing the text being drawn correctly by
TextDelegate
, but it’s not seen on the screen. That makes me think it’s a skiko issue, or the way it’s called from compose.
🙏 1
Also, this doesn’t seem to happen with
Copy code
System.setProperty("skiko.renderApi", "SOFTWARE_COMPAT")
But since it seems to be a race condition, it may be just accidental.
d
Thanks, we will check it
a
Ok, so I’ve dug deep into skiko and checked everything, and it all seems to work correctly. What I’ve done now is add a button (with no effects, so as not to cause redrawing) that grabs the toplevel
SkiaLayer
and dumps its screenshot into a file:
Copy code
val skiaLayer = (window.contentPane.components[0] as JLayeredPane).components[0] as SkiaLayer
ImageIO.write(skiaLayer.screenshot()?.toBufferedImage(), "png", File("$home/Desktop/compose.png"))
then I reproduce the issue (screen showing wrong value) and click the button. The screenshot shows the correct values.
d
Wow, amazing result! Thanks a lot for such detailed description!
a
Trying to get to the bottom of this…
The Picture
screenshot()
draws is the same Picture that was last drawn by
MetalContextHandler
on a Surface too. There’s nothing going on between the last draw to the screen and the screenshot.
even the native
org.jetbrains.skia.Surface
has the right thing drawn on it. I screenshotted it via
Surface.readPixels
. So it looks like the problem is further down, maybe in native code.