androidkotlinandroid-jetpack-composekotlin-multiplatform

LazyColumn Reverse Layout With Key Not Working - Auto Stick New Message to Bottom


I'm using Jetpack Compose and have a LazyColumn to display chat messages. The layout is in reverse order (reverseLayout = true) so that new messages appear at the bottom. However, when I use a key for each item (based on message.id), the latest message does not stick at the bottom as expected. Without using key it works fine. But when i using key it is sticks at a message in viewport filled with items so not scrolling i mean the new item not in bottom to push old item to top when using key.


@Composable
fun LazyColumnWithAddItemButton() {
    // State to hold the list of messages (items)
    val messages = remember { mutableStateListOf("Message 1", "Message 2", "Message 3") }

    // LazyListState to track scroll position
    val lazyListState = rememberLazyListState()

    // Button click handler to add a new message
    val addMessage = {
        messages.add("Message ${messages.size + 1}")
    }

    // Box allows us to layer the LazyColumn and Button, with the button fixed at the bottom
    Box(
        modifier = Modifier.fillMaxSize()
    ) {
        // LazyColumn with reverseLayout = true
        LazyColumn(
            state = lazyListState,
            modifier = Modifier
                .fillMaxWidth()
                .padding(bottom = 72.dp) // Add padding to ensure button is not overlapped
              .align(alignment = Alignment.BottomCenter),
            contentPadding = PaddingValues(horizontal = 16.dp),
            reverseLayout = true // Reverse layout so the first item is at the bottom
        ) {
            items(messages.reversed(), key = {it}) { message ->
                Text(
                    text = message,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
                        .background(Color.LightGray)
                        .padding(16.dp)
                )
            }
        }

        // Button fixed at the bottom of the screen
        Button(
            onClick = {
                addMessage() // Add new message to the list
            },
            modifier = Modifier
                .fillMaxWidth()
                .align(Alignment.BottomCenter) // Position button at the bottom
                .padding(16.dp)
        ) {
            Text("Add New Message")
        }
    }
}


Solution

  • When keys are used the current scroll position is anchored to the key. When new items are added the current scroll position is retained. That makes sense for a chat app: When the user currently scrolled up in the chat history and a new message comes in the scroll position shouldn't be changed to not distract the user from what they are doing.

    An exception would be when the user currently scrolled to the latest message: Now the expectation is that the chat is automatically scrolled to bring the new message into view.

    You can programmatically scroll the list using the lazyListState. To automatically scroll when a new message is added you can use a LaunchedEffect with the messages' size as key.

    To determine if the user scrolled to somewhere else so you can skip this automatic scrolling you can use lazyListState.firstVisibleItemIndex.

    This should do the trick (place directly after the lazyListState):

    val isScrolledToNewest =
        rememberSaveable(lazyListState.firstVisibleItemIndex) { lazyListState.firstVisibleItemIndex == 0 }
    
    LaunchedEffect(messages.size) {
        if (isScrolledToNewest) lazyListState.animateScrollToItem(0)
    }