Reputation: 560
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(...)
}
}
}
}
Upvotes: 5
Views: 7509
Reputation: 832
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)
}
Upvotes: 1
Reputation: 1344
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()
.
Upvotes: 2
Reputation: 522
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:
LazyColumn
is created.LazyColumn
is recomposed. We start again with asynchronously requesting pager data. Note: pager item count = 0 again!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:
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
Upvotes: 16
Reputation: 29885
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
Upvotes: 1