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.
This commit is contained in:
atsunatsu committed 2026-09-18 02:04:04 +08:00
1 parent 4dbe896124
commit 3cd5b49f98
2 files changed
+40

No files matched your search

@@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -49,11 +50,13 @@ import androidx.compose.material3.Slider
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.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -74,6 +77,7 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import kotlinx.coroutines.flow.distinctUntilChanged
@Composable @Composable
fun MutualScreen( fun MutualScreen(
@@ -150,6 +154,7 @@ fun MutualScreen(
} }
) { isVertical -> ) { isVertical ->
MutualContent( MutualContent(
viewModel = viewModel,
state = state, state = state,
isVertical = isVertical, isVertical = isVertical,
onQuery = viewModel::queryMutualPasses, onQuery = viewModel::queryMutualPasses,
@@ -172,6 +177,7 @@ fun MutualScreen(
@Composable @Composable
private fun MutualContent( private fun MutualContent(
viewModel: MutualViewModel,
state: MutualUiState, state: MutualUiState,
isVertical: Boolean, isVertical: Boolean,
onQuery: () -> Unit, 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( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState,
verticalArrangement = Arrangement.spacedBy(6.dp) verticalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
// Error message // Error message
@@ -69,6 +69,15 @@ class MutualViewModel(
private val _uiState = MutableStateFlow(MutualUiState()) private val _uiState = MutableStateFlow(MutualUiState())
val uiState: StateFlow<MutualUiState> = _uiState.asStateFlow() val uiState: StateFlow<MutualUiState> = _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 { init {
// Pre-fill station A with the user's current station position (as grid), // 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 // and default min elevation to the same value used by the main radar passes
@@ -201,6 +210,11 @@ class MutualViewModel(
selectedPassIndex = -1 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 { viewModelScope.launch {
val time = System.currentTimeMillis() val time = System.currentTimeMillis()