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.9k stars 1.16k forks source link

LazyColumn stickyHeader breaks in very specific edge case #757

Open Dominaezzz opened 3 years ago

Dominaezzz commented 3 years ago

This is the best I could reduce it to. If you can't reproducer, try making a bigger/maximised window.

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowSize
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import kotlinx.coroutines.flow.MutableSharedFlow

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
    application {
        val windowState = rememberWindowState(size = WindowSize(1280.dp, 720.dp))
        Window(windowState, title = "Sticky Header Issue!!!") {
            MaterialTheme {
                Reproducer()
            }
        }
    }
}

class Model(
    val list1: List<String> = emptyList(),
    val list2: List<String> = emptyList(),
    val list3: List<String> = emptyList(),
)

@Composable
fun Reproducer() {
    val model = remember { MutableSharedFlow<Model>(extraBufferCapacity = 1) }

    Column(Modifier.fillMaxWidth(0.3f)) {
        Row(Modifier.fillMaxWidth(), Arrangement.spacedBy(8.dp), Alignment.CenterVertically) {
            Spacer(Modifier.width(5.dp))

            OutlinedTextField(
                value = "roomFilter",
                onValueChange = {  },
                modifier = Modifier.weight(1f),
                placeholder = { Text("Filter...") },
                leadingIcon = { Icon(Icons.Filled.FilterList, null) }
            )
        }

        Column {
            Button(
                onClick = {
                    val stuff = (1..20).map { "Item $it" }
                    val res = model.tryEmit(Model(stuff, stuff, stuff))
                    check(res)
                }
            ) {
                Text("Show Bug!")
            }

            val modelProxy = model.collectAsState(Model()).value
            // val modelProxy by model.collectAsState(Model()) // This works?!?!?!

            LazyColumn {
                fun section(header: String, list: List<String>) {
                    @OptIn(ExperimentalFoundationApi::class)
                    stickyHeader { Text(header, style = MaterialTheme.typography.subtitle1) }
                    items(list) { Text(it) }
                }

                section("Header1", modelProxy.list1)
                section("Header2", modelProxy.list2)
                section("Header3", modelProxy.list3)
                section("Header4", modelProxy.list3)
            }
        }
    }
}

Before clicking the "Show Bug!" button you should see this. sticky_header_before

After clicking the "Show Bug!" button you should see this. sticky_header_after

I expected to see this instead. (You can see this if you use the other modelProxy declaration). sticky_header_expected

I'm not 100% sure why val modelProxy = model.collectAsState(Model()).value breaks but val modelProxy by model.collectAsState(Model()) works.

I'm on Arch Linux (i3 & X11) and Compose-Desktop 0.4.0.

okushnikov commented 2 weeks ago

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