I have a MyView
Composable which consists of a Column
housing a LazyColumn
which contains some header rows, content rows, and some LazyRows as well that scroll horizontally. When I navigate forward from this using navController.navigate(route)
and then return using navController.popBackStack()
the scroll state of the LazyColumn
is not retained and it is always positioned back at the top.
I can't figure out why this occurs. The ViewModel is not being re-created, the Flows which populate the LazyRows are not emitting new values, and the LazyRows are retaining their horizontal scroll position. I'm using the same navigation elsewhere in the app and all other LazyColumns are retaining their scroll positions through navigation without issue.
A simplified version of my composable is as follows:
@Composable
fun MyView(
viewModel: MyViewModel,
onNavigateToNext: (String) -> Unit,
) {
val introItems by viewModel.introItems.collectAsStateWithLifecycle(emptyList())
val row1items by viewModel.row1items.collectAsStateWithLifecycle(emptyList())
val row2items by viewModel.row2items.collectAsStateWithLifecycle(emptyList())
val row3items by viewModel.row3items.collectAsStateWithLifecycle(emptyList())
val row4items by viewModel.row4items.collectAsStateWithLifecycle(emptyList())
val listState = rememberLazyListState()
Column(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
state = listState,
contentPadding = PaddingValues(bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(horizontal = 8.dp)
.windowInsetsPadding(WindowInsets.ime)
) {
item(key = "header1") {
Text("header1", modifier = Modifier.height(80.dp))
}
items(introItems, key = {it.itemID}) { listItem ->
Text(listItem.name, modifier = Modifier.height(80.dp))
}
item(key = "header2") {
Text("header2", modifier = Modifier.height(80.dp))
}
item(key = "lazyrow1") {
LazyRow {
items(row1items) { listItem ->
Text(listItem.name, modifier = Modifier.height(150.dp))
}
}
}
item(key = "header3") {
Text("header3", modifier = Modifier.height(80.dp))
}
item(key = "lazyrow2") {
LazyRow {
items(row2items) { listItem ->
Text(listItem.name, modifier = Modifier.height(150.dp))
}
}
}
item(key = "header4") {
Text("header4", modifier = Modifier.height(80.dp))
}
item(key = "lazyrow3") {
LazyRow {
items(row3items) { listItem ->
Text(listItem.name, modifier = Modifier.height(150.dp))
}
}
}
item(key = "header5") {
Text("header5", modifier = Modifier.height(80.dp))
}
item(key = "lazyrow4") {
LazyRow {
items(row4items) { listItem ->
Text(
listItem.name,
modifier = Modifier
.height(150.dp)
.clickable { onNavigateToNext(listItem.itemID.toString()) }
)
}
}
}
}
}
}
The Flows are implemented in the ViewModel in this manner:
@OptIn(ExperimentalCoroutinesApi::class)
val row1Items: StateFlow<List<MyEntity>> = query
.flatMapLatest { queryParams ->
myDao.getItemsFlow(queryParams.query)
}
.map { list ->
sortedItems(list)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5.seconds),
initialValue = emptyList(),
)
Basically, there is a query data class that, when modified, calls a DAO query method, and sorts the results from that DAO query before emitting the sorted list to the flow.
I can't test your code right now and thus don't have a definitive solution, but some suggestions to try narrow down the issue.
It could be happening that according to your Flow definition, the Flow stops emitting values five seconds after all subscribers are gone. Now, once you return to this Composable, for a short moment in time, the initialValue
is applied, which is an emptyList
, and the scroll position thus could automatically reset to 0
.
You can try the following things to confirm whether that is the actual issue:
You could only show the LazyColumn
when there are items present. This should prevent the scroll position to be reset when the introItems
are empty for a short moment in time.
if (introItems.isNotEmpty()) {
LazyColumn(
state = listState,
contentPadding = PaddingValues(bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(horizontal = 8.dp)
.windowInsetsPadding(WindowInsets.ime)
) {
//...
}
}
You could prevent the Flow
from stop sharing data after the subscribers are gone by changing the stateIn
configuration to SharingStarted.Lazily
. Then, the Flow will start emitting values once the first subscriber appears, but will remain active even when all subscribers are gone. Beware that this might not be a good solution depending on the structure of the app.
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily, // will start with the
initialValue = emptyList(),
)
If either of these approaches change the problematic behavior, we have tracked down the issue.
Can you please test and report back whether either of these two things changes anything?