fix(android): reduce chat recomposition churn

This commit is contained in:
Ayaan Zaidi 2026-03-16 18:42:20 +05:30
parent 3009e689bc
commit 56e23a887f
No known key found for this signature in database
2 changed files with 18 additions and 7 deletions

View File

@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -34,11 +36,19 @@ fun ChatMessageListCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
val displayMessages = remember(messages) { messages.asReversed() }
val stream = streamingAssistantText?.trim()
// With reverseLayout the newest item is at index 0 (bottom of screen). // New list items/tool rows should animate into view, but token streaming should not restart
LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size, streamingAssistantText) { // that animation on every delta.
LaunchedEffect(messages.size, pendingRunCount, pendingToolCalls.size) {
listState.animateScrollToItem(index = 0) listState.animateScrollToItem(index = 0)
} }
LaunchedEffect(stream) {
if (!stream.isNullOrEmpty()) {
listState.scrollToItem(index = 0)
}
}
Box(modifier = modifier.fillMaxWidth()) { Box(modifier = modifier.fillMaxWidth()) {
LazyColumn( LazyColumn(
@ -50,8 +60,6 @@ fun ChatMessageListCard(
) { ) {
// With reverseLayout = true, index 0 renders at the BOTTOM. // With reverseLayout = true, index 0 renders at the BOTTOM.
// So we emit newest items first: streaming → tools → typing → messages (newest→oldest). // So we emit newest items first: streaming → tools → typing → messages (newest→oldest).
val stream = streamingAssistantText?.trim()
if (!stream.isNullOrEmpty()) { if (!stream.isNullOrEmpty()) {
item(key = "stream") { item(key = "stream") {
ChatStreamingAssistantBubble(text = stream) ChatStreamingAssistantBubble(text = stream)
@ -70,8 +78,8 @@ fun ChatMessageListCard(
} }
} }
items(count = messages.size, key = { idx -> messages[messages.size - 1 - idx].id }) { idx -> items(items = displayMessages, key = { it.id }) { message ->
ChatMessageBubble(message = messages[messages.size - 1 - idx]) ChatMessageBubble(message = message)
} }
} }

View File

@ -160,7 +160,10 @@ private fun ChatThreadSelector(
mainSessionKey: String, mainSessionKey: String,
onSelectSession: (String) -> Unit, onSelectSession: (String) -> Unit,
) { ) {
val sessionOptions = resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey) val sessionOptions =
remember(sessionKey, sessions, mainSessionKey) {
resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey)
}
Row( Row(
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),