From 3cd5b49f987e158a082a221dee84689dbabe59ca Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Fri, 18 Sep 2026 02:04:04 +0800 Subject: [PATCH] feat(mutual): keep pass list scroll position across page switches The mutual pass list LazyColumn reset to the top on every return to the page. Scroll position is now stored in the Activity-scoped MutualViewModel and restored on re-entry; a new query (queryGeneration bump) rebuilds the list at the top so stale positions never leak into fresh results. --- .../look4sat/feature/mutual/MutualScreen.kt | 26 +++++++++++++++++++ .../feature/mutual/MutualViewModel.kt | 14 ++++++++++ 2 files changed, 40 insertions(+) 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 f2e1ccc0..79e5ae45 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 @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Button @@ -49,11 +50,13 @@ import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf 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.res.painterResource @@ -74,6 +77,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone +import kotlinx.coroutines.flow.distinctUntilChanged @Composable fun MutualScreen( @@ -150,6 +154,7 @@ fun MutualScreen( } ) { isVertical -> MutualContent( + viewModel = viewModel, state = state, isVertical = isVertical, onQuery = viewModel::queryMutualPasses, @@ -172,6 +177,7 @@ fun MutualScreen( @Composable private fun MutualContent( + viewModel: MutualViewModel, state: MutualUiState, isVertical: Boolean, onQuery: () -> Unit, @@ -195,8 +201,28 @@ private fun MutualContent( } } + // List state survives page switches via the Activity-scoped ViewModel: the + // initial position is restored from the VM, and scrolling writes back to it, + // so returning to this page keeps exactly where the user left off. A new + // query (queryGeneration bump) rebuilds the state at the top. + val listState = remember(viewModel.queryGeneration) { + LazyListState( + firstVisibleItemIndex = viewModel.listScrollIndex, + firstVisibleItemScrollOffset = viewModel.listScrollOffset + ) + } + LaunchedEffect(listState) { + snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } + .distinctUntilChanged() + .collect { (index, offset) -> + viewModel.listScrollIndex = index + viewModel.listScrollOffset = offset + } + } + LazyColumn( modifier = Modifier.fillMaxSize(), + state = listState, verticalArrangement = Arrangement.spacedBy(6.dp) ) { // Error message diff --git a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualViewModel.kt b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualViewModel.kt index 7520f5a9..66abd1ec 100644 --- a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualViewModel.kt +++ b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualViewModel.kt @@ -69,6 +69,15 @@ class MutualViewModel( private val _uiState = MutableStateFlow(MutualUiState()) val uiState: StateFlow = _uiState.asStateFlow() + // Session-scoped scroll position of the pass list (Activity-scoped VM, so it + // survives switching to another page and back). Reset to the top whenever a + // new query replaces the results. + var listScrollIndex: Int = 0 + var listScrollOffset: Int = 0 + + /** Bumped on every new query so the list scrolls back to the top. */ + var queryGeneration: Int = 0 + init { // Pre-fill station A with the user's current station position (as grid), // and default min elevation to the same value used by the main radar passes @@ -201,6 +210,11 @@ class MutualViewModel( selectedPassIndex = -1 ) } + // The results list is about to be replaced, so the scroll position must + // not leak from the previous query's list. + listScrollIndex = 0 + listScrollOffset = 0 + queryGeneration += 1 viewModelScope.launch { val time = System.currentTimeMillis()