From d600836e19c30b30af5dd589f2f5a775806b4fe8 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Tue, 22 Sep 2026 14:49:00 +0800 Subject: [PATCH] fix(mutual): gate prefill scroll on real list layout (onGloballyPositioned) On a device the Mutual entry is composed inside the NavDisplay transition; LaunchedEffect could run before the LazyColumn is measurable and the scrollToItem then never lands, leaving the page at the top. Only scroll once onGloballyPositioned confirms the list has actually been laid out. --- .../look4sat/feature/mutual/MutualScreen.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt index 19ccc658..1a104283 100644 --- a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt +++ b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt @@ -54,11 +54,13 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -220,8 +222,18 @@ private fun MutualContent( } } + // Set once the list has actually been laid out. The prefill scroll only + // fires after this, because on a real device the Mutual entry appears + // through the NavDisplay transition and LaunchedEffect alone can run + // before the list is measurable (the scroll then never lands and the + // page stays at the top). onGloballyPositioned fires on the first real + // layout, so the subsequent scrollToItem always has a measured list. + var listReady by remember { mutableStateOf(false) } + LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { listReady = true }, state = listState, verticalArrangement = Arrangement.spacedBy(6.dp) ) { @@ -377,8 +389,8 @@ private fun MutualContent( // LazyListState at index 1: the constructor parameter is ignored by this // Compose version (verified in tests), while scrollToItem lands correctly. val matchSearchIndex = if (state.errorMessage != null) 2 else 1 - LaunchedEffect(state.scrollToTimeRange, matchSearchIndex) { - if (state.scrollToTimeRange) { + LaunchedEffect(state.scrollToTimeRange, matchSearchIndex, listReady) { + if (state.scrollToTimeRange && listReady) { listState.scrollToItem(matchSearchIndex) viewModel.consumeScrollToTimeRange() }