Scroll the chat by request, not by forcing a layout
The chat keeps its newest message in view when it opens and while the
keyboard slides, and both did it with `listState.scrollToItem`, which forces
a layout on the spot. This screen is composed inside the Scaffold's measure
pass, so when the effect ran straight away it asked for a layout in the
middle of one:
IllegalArgumentException: performMeasureAndLayout called during measure layout
at LayoutNode.forceRemeasure
at LazyListState.snapToItemIndexInternal
at ChatScreens.kt:1740 (the keyboard follow)
... LaunchedEffectImpl.onRemembered ... SubcomposeLayout ... Scaffold
It failed ChatFlowTest's send in every full on-device run since 2026-09-18.
The frames that ran the effect inline are the Compose test dispatcher's; the
shipped app posts effects to the next frame, so this has not been seen to
crash a real device. Forcing a synchronous layout from an effect is wrong
either way.
Both now use `requestScrollToItem` (foundation 1.7.6), which the next layout
applies, and the keyboard is what brings that layout. Still instant, still
not animated. The animated follow for new messages is unchanged: it waits
for animation frames before it moves.
TheKeyboardDoesNotHideTheMessagesTest read the source for
`listState.scrollToItem(`; it now requires `requestScrollToItem(` and refuses
`scrollToItem(` in the keyboard effect. Put the old line back and it fails.
Result on relay1: the full on-device suite passes, 10 of 10, and the crash
buffer has no performMeasureAndLayout. App unit tests: 358 debug, 323
release, all green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a44624cfad
commit
d33bb338de
@@ -1715,8 +1715,17 @@ fun ColumnScope.ChatScreen(
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// Open AT the newest message, a jump, not a tour of the history.
|
||||
//
|
||||
// `requestScrollToItem`, not `scrollToItem`, here and for the keyboard
|
||||
// below. `scrollToItem` forces a layout on the spot, and this screen is
|
||||
// composed inside the Scaffold's MEASURE pass, so an effect that runs
|
||||
// straight away asks for a layout in the middle of one:
|
||||
// "performMeasureAndLayout called during measure layout", thrown from the
|
||||
// keyboard effect in the on-device suite (ChatFlowTest, 2026-09-18 to
|
||||
// 09-28). The request is applied by the next layout instead, which comes
|
||||
// at once because the keyboard is what is changing the layout.
|
||||
LaunchedEffect(Unit) {
|
||||
if (chat.lines.isNotEmpty()) listState.scrollToItem(chat.lines.lastIndex)
|
||||
if (chat.lines.isNotEmpty()) listState.requestScrollToItem(chat.lines.lastIndex)
|
||||
}
|
||||
// Follow the conversation as it grows.
|
||||
LaunchedEffect(chat.lines.size) {
|
||||
@@ -1737,7 +1746,7 @@ fun ColumnScope.ChatScreen(
|
||||
// of that lands short by however much the viewport shrank afterwards.
|
||||
val imeBottom = WindowInsets.ime.getBottom(LocalDensity.current)
|
||||
LaunchedEffect(imeBottom) {
|
||||
if (chat.lines.isNotEmpty()) listState.scrollToItem(chat.lines.lastIndex)
|
||||
if (chat.lines.isNotEmpty()) listState.requestScrollToItem(chat.lines.lastIndex)
|
||||
}
|
||||
|
||||
// One bar, not three stacked rows, and it wears the channel's own colour.
|
||||
|
||||
+11
-2
@@ -58,10 +58,19 @@ class TheKeyboardDoesNotHideTheMessagesTest {
|
||||
// The inset arrives as a series of values while the keyboard slides.
|
||||
// One animated jump fired at the start of that lands short by however
|
||||
// much the viewport shrank afterwards.
|
||||
//
|
||||
// Instant means `requestScrollToItem`, applied by the next layout, and
|
||||
// NOT `scrollToItem`, which forces a layout on the spot: this screen is
|
||||
// composed inside the Scaffold's measure pass, and on-device that threw
|
||||
// "performMeasureAndLayout called during measure layout".
|
||||
val block = screens.substringAfter("LaunchedEffect(imeBottom) {").substringBefore("}")
|
||||
assertTrue(
|
||||
block.contains("listState.scrollToItem("),
|
||||
"the keyboard follow should scroll instantly: $block",
|
||||
block.contains("listState.requestScrollToItem("),
|
||||
"the keyboard follow should scroll instantly, by request: $block",
|
||||
)
|
||||
assertTrue(
|
||||
!block.contains("listState.scrollToItem("),
|
||||
"scrollToItem forces a layout and throws when it runs inside one: $block",
|
||||
)
|
||||
assertTrue(
|
||||
!block.contains("animateScrollToItem"),
|
||||
|
||||
Reference in New Issue
Block a user