How to save paging state of LazyColumn during navigation in Jetpack Compose
Asked Answered
E

4

5

I'm using androidx.paging:paging-compose (v1.0.0-alpha-14), together with Jetpack Compose (v1.0.3), I have a custom PagingSource which is responsible for pulling items from backend. I also use compose navigation component.

The problem is I don't know how to save a state of Pager flow between navigating to different screen via NavHostController and going back (scroll state and cached items). I was trying to save state via rememberSaveable but it cannot be done as it is not something which can be putted to Bundle. Is there a quick/easy step to do it?

My sample code:

@Composable
fun SampleScreen(
   composeNavController: NavHostController? = null,
   myPagingSource: PagingSource<Int, MyItem>,
) {
   val pager = remember { // rememberSaveable doesn't seems to work here
       Pager(
           config = PagingConfig(
               pageSize = 25,
           ),
           initialKey = 0,
           pagingSourceFactory = myPagingSource
       )
   }
   val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

   LazyColumn() {
       itemsIndexed(items = lazyPagingItems) { index, item ->
           MyRowItem(item) {
               composeNavController?.navigate(...)
           }
       }
   }
}
Exurb answered 27/10, 2021 at 12:55 Comment(1)
Has anyone found a solution for this issue?Bergh
P
15

I found a solution!

@Composable
fun Sample(data: Flow<PagingData<Something>>):
    val listState: LazyListState = rememberLazyListState()
    val items: LazyPagingItems<Something> = data.collectAsLazyPagingItems()

    when {
        items.itemCount == 0 -> LoadingScreen()
        else -> {
            LazyColumn(state = listState, ...) {
                ...
            }
        }
    }
    ...

I just found out what the issue is when using Paging.

The reason the list scroll position is not remembered with Paging when navigating boils down to what happens below the hood. It looks like this:

  1. Composable with LazyColumn is created.
  2. We asynchronously request our list data from the pager. Current pager list item count = 0.
  3. The UI draws a lazyColumn with 0 items.
  4. The pager responds with data, e.g. 10 items, and the UI is recomposed to show them.
  5. User scrolls e.g. all the way down and clicks the bottom item, which navigates them elsewhere.
  6. User navigates back using e.g. the back button.
  7. Uh oh. Due to navigation, our composable with LazyColumn is recomposed. We start again with asynchronously requesting pager data. Note: pager item count = 0 again!
  8. rememberLazyListState is evaluated, and it tells the UI that the user scrolled down all the way, so it now should go back to the same offset, e.g. to the fifth item.

This is the point where the UI screams in wild confusion, as the pager has 0 items, so the lazyColumn has 0 items. The UI cannot handle the scroll offset to the fifth item. The scroll position is set to just show from item 0, as there are only 0 items.

What happens next:

  1. The pager responds that there are e.g. 10 items again, causing another recomposition.
  2. After recomposition, we see our list again, with scroll position starting on item 0.

To confirm this is the case with your code, add a simple log statement just above the LazyColumn call:

Log.w("TEST", "List state recompose. " +
            "first_visible=${listState.firstVisibleItemIndex}, " +
            "offset=${listState.firstVisibleItemScrollOffset}, " +
            "amount items=${items.itemCount}")

You should see, upon navigating back, a log line stating the exact same first_visible and offset, but with amount items=0. The line directly after that will show that first_visible and offset are reset to 0.

My solution works, because it skips using the listState until the pager has loaded the data. Once loaded, the correct values still reside in the listState, and the scroll position is correctly restored.

Source: https://issuetracker.google.com/issues/177245496

Purine answered 29/12, 2021 at 13:58 Comment(3)
With this, How will it load for the first time loading ?Nidianidicolous
@Nidianidicolous items is loaded by collecting data (a paging flow). This flow is responsible for loading. A repository usually is the source of data. A viewmodel gets the data as a paging flow. My code example consumes such a flow and displays the items inside. Loading is handled by the paging library. For more information, see the official paging documentation herePurine
it worked with some case with LazyListState, thanks a lot !!!!Depone
R
2

The issue is that when you navigate forward and back your composable will recompose and collectAsLazyPagingItems() will be called again, triggering a new network request.

If you want to avoid this issue, you should call pager.flow.cacheIn(viewModelScope) on your ViewModel with activity scope (the ViewModel instance is kept across fragments) before calling collectAsLazyPagingItems().

Rico answered 2/8, 2022 at 14:52 Comment(2)
this works for me, should be signed as correct answer, thanksZaccaria
This saved the data (stopping any further api download), but did not keep the scrolling state for me.Gully
T
1

Save the list state in your viewmodel and reload it when you navigate back to the screen containing the list. You can use LazyListState in your viewmodel to save the state and pass that into your composable as a parameter. Something like this:

class MyViewModel: ViewModel() {
   var listState = LazyListState()
}

@Composable
fun MessageListHandler() {

   MessageList(
      messages: viewmodel.messages,
      listState = viewmode.listState
   )
}

@Composable
fun MessageList(
   messages: List<Message>,
   listState: LazyListState) {

    LazyColumn(state = listState) {

    }
}

If you don't like the limitations that Navigation Compose puts on you, you can try using Jetmagic. It allows you to pass any object between screens and even manages your viewmodels in a way that makes them easier to access from any composable:

https://github.com/JohannBlake/Jetmagic

Taiga answered 27/10, 2021 at 14:39 Comment(1)
This is not working together with PagingSource.Exurb
M
1

LazyPagingItems is not intended as a persistent data store; it is just a simple wrapper for the UI layer. Pager data should be cached in the ViewModel. please try using '.cachedIn(viewModelScope) '

simple example:

@Composable
fun Simple() {
    val simpleViewModel:SimpleViewModel = viewModel()
    val list = simpleViewModel.simpleList.collectAsLazyPagingItems()
    when (list.loadState.refresh) {
        is LoadState.Error -> {
            //..
        }
        is LoadState.Loading -> {
            BoxProgress()
        }
        is LoadState.NotLoading -> {
                when (list.itemCount) {
                    0 -> {
                        //..
                    }
                    else -> {
                        LazyColumn(){
                            items(list) { b ->
                               //..
                            }
                        }
                    }
                }
        }
    }
    //..
}

class SimpleViewModel : ViewModel() {
    val simpleList = Pager(
        PagingConfig(PAGE_SIZE),
        pagingSourceFactory = { SimpleSource() }).flow.cachedIn(viewModelScope)
}
Musclebound answered 30/8, 2022 at 9:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.