androidkotlinandroid-jetpack-compose

LazyColumn losing scroll position after navigating forward and popping back


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.


Solution

  • 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:

    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?