JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.87k stars 1.15k forks source link

Compose-desktop @Preview fails to show preview on two icon buttons and hangs up #2472

Open vdshb opened 1 year ago

vdshb commented 1 year ago

Desktop previewer fails to show a preview for this snippet:

package com.example

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.runtime.Composable

@Preview
@Composable
fun Reproducer(
) {
    IconButton(onClick = {}) {
        Icon(
            Icons.Filled.ArrowDropDown,
            contentDescription = "A",
        )
    }
    IconButton(onClick = {}) {
        Icon(
            Icons.Filled.ArrowDropDown,
            contentDescription = "B",
        )
    }
}

After try, it hangs up and doesn't show any other preview as well. Only project reload helps to reset the error.

kotlin version: 1.7.20 compose version: 1.2.1 or 1.3.0-alpha01-dev853

vdshb commented 1 year ago

I've faced similar behaviour when I switched between two previews:

@Preview
@Composable
fun Reproducer2(
) {
    Icon(Icons.Filled.Add, contentDescription = "A")
}

@Preview
@Composable
fun Reproducer3(
) {
    Icon(Icons.Filled.ArrowDropDown, contentDescription = "A")
}

First couple of renders goes fine, then it's stuck and stop previewing anything.

Maybe it's a different issue, but I believe they are related.

dsvoronin commented 1 year ago

Yep, same thing, and I can't see the common reason in my code for this

https://github.com/JetBrains/compose-jb/issues/2156#issuecomment-1284492736

dsvoronin commented 1 year ago

Looks like IDE somehow looses track of port used to connect to PreviewHost. If I am reading it correctly

image

configureDesktopPreview -Pcompose.desktop.preview.target=<MyPReviewClass> -Pcompose.desktop.preview.ide.port=36207'

If it's actually need to be the same, I am not sure. IDE exception log is in the comment above

malliaridis commented 1 year ago

I analyzed the error myself because my team and I are facing this issue for quite a while now. A added a few more system outputs to reduce the scope for search and ended up seeing the following for successful previews:

CONNECTION (PREVIEW) #2:SENT COMMAND 'FRAME_REQUEST com.example.common.TestUiPreviewKt.TestUiPreview 1 632 912 4607182418800017408'
SERVER:PREVIEW_HOST:GOT COMMAND 'PREVIEW_CLASSPATH'
SERVER:PREVIEW_HOST:GOT [4474]
SERVER:PREVIEW_HOST:GOT COMMAND 'FRAME_REQUEST com.example.common.TestUiPreviewKt.TestUiPreview 1 632 912 4607182418800017408'
SERVER:PREVIEW_HOST:RENDERING 'com.example.common.TestUiPreviewKt.TestUiPreview' 632x912@1.0
2023-01-28 03:36:40,770 [  35396]   WARN -       GradleBuildOutputUtil.kt - Failed to read Json output file from C:\Users\Christos\workspace\temp\ide-plugin-debug\android\build\outputs\apk\debug\output-metadata.json. Build may have failed. 
SERVER:PREVIEW_HOST:RENDERED [4966] in 571 ms
SERVER:PREVIEW_HOST:SENT COMMAND 'FRAME 632 912'
SERVER:PREVIEW_HOST:SENT DATA [4966]
CONNECTION (PREVIEW) #2:GOT COMMAND 'FRAME 632 912'
CONNECTION (PREVIEW) #2:GOT [4966]

When the preview fails and stops working the output is:

CONNECTION (PREVIEW) #2:SENT COMMAND 'FRAME_REQUEST com.example.common.TestUiPreviewKt.TestUiPreview 3 632 912 4607182418800017408'
SERVER:PREVIEW_HOST:GOT COMMAND 'PREVIEW_CLASSPATH'
SERVER:PREVIEW_HOST:GOT [4474]
SERVER:PREVIEW_HOST:GOT COMMAND 'FRAME_REQUEST com.example.common.TestUiPreviewKt.TestUiPreview 3 632 912 4607182418800017408'
SERVER:PREVIEW_HOST:RENDERING 'com.example.common.TestUiPreviewKt.TestUiPreview' 632x912@1.0

It never stops rendering.

In RemotePreviewHost there is the following line:

val render = try {
  previewFacade.getMethod("render", *renderArgsClasses)
} // ...

that fetches the method for rendering, and a few lines further there is the block for rendering:

val ms = measureTimeMillis {
    bytes = render.invoke(previewFacade, fqName, scaledWidth, scaledHeight, scale) as ByteArray
}

So it seems like there is a bug somewhere in the preview facade class androidx.compose.desktop.ui.tooling.preview.runtime.NonInteractivePreviewFacade. I do not know where to find the source code of this class to further analyze the bug.

I was reproducing the error with the following code snippet:

package com.example.common

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp

@Preview
@Composable
fun TestUiPreview() {
    MaterialTheme {
        ComponentUi()
    }
}

@Composable
fun ComponentUi() {
    Column(
        modifier = Modifier.padding(horizontal = 16.dp, vertical = 64.dp),
        verticalArrangement = Arrangement.Top
    ){
        Text(
            text = "Login",
            textAlign = TextAlign.Start,
            style = MaterialTheme.typography.h3
        )
        OutlinedTextField(
            modifier = Modifier.fillMaxWidth(),
            value = "model.email 2",
            leadingIcon = { Icon(painter = rememberVectorPainter(Icons.Default.Email), contentDescription = "") },
            onValueChange = {},
            placeholder = { Text("Email Address") },
            singleLine = true,
        )
        // IMPORTANT Uncomment below text field to cause the bug
//        OutlinedTextField(
//            modifier = Modifier.fillMaxWidth(),
//            value = "model.password",
//            onValueChange = {},
//            placeholder = { Text("Password") },
//            singleLine = true,
//        )
    }
}

All compose dependencies were updated to 1.2.2 and the preview was placed in the desktopMain modoule of a multiplatform project.

@dsvoronin I tried to analyze your statement but couldn't find any misbehavior in the connection / socket.

EDIT: The UI is not updated anymore after the bug occurs because new rendering requests are blocked due to the already running rendering process. This can be seen in PreviewManager:

if (inProcessRequest.get() == null && request != prevRequest) { // is never true after bug
  // ...
}
kirill-grouchnikov commented 1 year ago

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/runtime/NonInteractivePreviewFacade.kt;l=46?q=androidx.compose.desktop.ui.tooling.preview.runtime.NonInteractivePreviewFacade&sq=

malliaridis commented 1 year ago

I wasn't able to further debug the problem because the test project with the development IDE was not using directly the preview-rpc module for rendering and I am not aware how to setup everything to debug it properly.

However, my assumption is that it is somehow related to the rendering of a Column with rememberVectorPainter as a (nested) child. When the rememberVectorPainter is not used at all or used only in the last element of the column, the rendering works just fine. Once an element comes after the element that uses rememberVectorPainter (e.g. Icon(imageVector =...) or OutlinedTextField(leadingIcon= ...), the rendering never completes and any follow-up rendering requests are skipped due to the incomplete rendering process. I was assuming that it was only happening when using rememberVectorPainter twice, but that is not necessary.

A workaround until the bug is fixed would be to use LazyColumn with item{...}, or not use rememberVectorPainter inside a Column.

okushnikov commented 1 week ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.