9

I am using Jetpack Compose, along with Paging 3 library & Jetpack Navigation. The issue I am facing is I have a LazyList which is fetching data from remote source using paging library.

ViewModel

fun getImages(): Flow<PagingData<ObjectImage>> = Pager(
        PagingConfig(PAGE_SIZE, enablePlaceholders = false)
    ) { DataHome(RANDOM) }.flow.cachedIn(viewModelScope)

HomeView

val images = viewModelHome.getImages().collectAsLazyPagingItems()
LazyColumn {
  ...
}

Now whats happening is when I navigate to another View using navHostController.navigate() and then press back to get to HomeView... the LazyColumn resets itself & start loading items again from network.

So I am stuck with this issue. I tried manually caching in viewModel variable... though it works but it screws up SwipeRefresh (which stops showing refresh state)

data.apply {
            when {
                // refresh
                loadState.refresh is LoadState.Loading -> {
                    ItemLoading()
                }

                // reload
                loadState.append is LoadState.Loading -> {...}

                // refresh error
                loadState.refresh is LoadState.Error -> {...}

                // reload error
                loadState.append is LoadState.Error -> {...}
            }
        }
implementation("androidx.paging:paging-runtime-ktx:3.1.0")
implementation("androidx.paging:paging-compose:1.0.0-alpha14")

Is this an issue with PagingLibrary which is still in alpha??

Update 1 (I am not sure if this is a good solution, but I am solving the swipe refresh issue as follows)

// get images
    var images: Flow<PagingData<ObjectImage>> = Pager(PagingConfig(PAGE_SIZE)) {
        DataHome(RANDOM)
    }.flow.cachedIn(viewModelScope)

    // reload items
    fun reload(){
        images = Pager(PagingConfig(PAGE_SIZE)) {
            DataHome(RANDOM)
        }.flow.cachedIn(viewModelScope)
    }

// and rather than calling .refresh() method on lazy items... I am calling viewModel.reload()
Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Saksham Khurana
  • 872
  • 13
  • 26

1 Answers1

13

The problem is that you are creating new Pager every time you call getImages(), which is every time your composable recomposes, that's not how it's supposed to be done.

You should make it a val items = Pager(... for the caching to work.

For the screwed up SwipeRefresh, how do you implement it? There is a refresh() method on LazyPagingItems, you should use that.


EDIT: Ok, so based on the coments and edits to your question:

In your viewmodel, do as I suggested before:

val items = Pager( // define your pager here

Your composable can then look like this:

@Composable
fun Screen() {
    val items = viewModel.items.collectAsLazyPagingItems()
    val state = rememberSwipeRefreshState(
        isRefreshing = items.loadState.refresh is LoadState.Loading,
    )

    SwipeRefresh(
        modifier = Modifier.fillMaxSize(),
        state = state,
        // use the provided LazyPagingItems.refresh() method,
        // no need for custom solutions
        onRefresh = { items.refresh() }
    ) {
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
        ) {
            // display the items only when loadState.refresh is not loading,
            // as you wish
            if (items.loadState.refresh is LoadState.NotLoading) {
                items(items) {
                    if (it != null) {
                        Text(
                            modifier = Modifier.padding(16.dp),
                            text = it,
                        )
                    }
                }
                // you can also add item for LoadState.Error, anything you want
                if (items.loadState.append is LoadState.Loading) {
                    item {
                        Box(modifier = Modifier.fillMaxWidth()) {
                            CircularProgressIndicator(
                                modifier = Modifier
                                    .align(Alignment.Center)
                                    .padding(16.dp)
                            )
                        }
                    }
                }
            }
            // if the loadState.refresh is Loading,
            // display just single loading item,
            // or nothing at all (SwipeRefresh already indicates
            // refresh is in progress)
            else if (items.loadState.refresh is LoadState.Loading) {
                item {
                    Box(modifier = Modifier.fillParentMaxSize()) {
                        Text(
                            text = "Refreshing",
                            modifier = Modifier.align(Alignment.Center))
                    }
                }
            }
        }
    }
}
Jan Bína
  • 3,447
  • 14
  • 16
  • but isn't `flow.cachedIn(viewModelScope)` suppose to cache the result in viewModel & check that if cache is available use that else fire a new request ? And for second part (considering I cache in a variable) yes I am using `refresh()` method , it is triggering a new request to the network but not clearing the existing items (which is turn shows the `loadState.refresh is LoadState.Loading`) before making the call – Saksham Khurana Mar 10 '22 at 03:44
  • 1
    Yes, it caches the result in viewModel, but when you call `getImages()`, you create whole new `Pager` object, which doesn't know anything about the previous one so it starts empty. "*but not clearing the existing items*" - this is **correct** and intended behavior - it is valid usecase to keep showing old data while loading fresh data - take e.g. gmail, it shows already downloaded emails while checking for new... if you don't want that, check for `loadState.refresh` and don't display your data if it's `LoadState.Loading` – Jan Bína Mar 10 '22 at 12:10
  • first - then how is that cache used & when? to be honest all examples including google's use the way I am doing only (that's why doing that) hence I am very confused. second - yes I don't want to show anything & yes I am using `loadState.refresh is LoadState.Loading` but it is still displaying the data & not the loadState I intend... and just to clarify if I use normal way (without var caching) it is properly refreshing the way I intended... so just switching to caching in var is what screwing up the refresh – Saksham Khurana Mar 11 '22 at 05:43
  • one more thing I saw now, that when caching with variable `loadState.refresh is LoadState.Loading` is adding the loading item at the end of the list without clearing the existing list – Saksham Khurana Mar 11 '22 at 06:17
  • When you stop collecting the paging flow (for example when you navigate to other screen), then the cache is used. Without `cachedIn(viewModelScope)`, it will discard loaded data in that case. – Jan Bína Mar 11 '22 at 08:58
  • What you can do is something like `if (loadState.refresh is LoadState.Loading) { LazyColumn() } else { ProgressIndicator() }`. Like this, the data clearly won't be displayed while loding. "*all examples including google's use the way I am doing only*" - can you link some please, I've never seen that – Jan Bína Mar 11 '22 at 09:01
  • https://developer.android.com/jetpack/compose/lists#large-datasets https://proandroiddev.com/infinite-lists-with-paging-3-in-jetpack-compose-b095533aefe6 – Saksham Khurana Mar 11 '22 at 11:20
  • yes I am using all cases to show the states; refresh loading, refresh error, append loading, append error... in initial load the refresh is showing properly but when swipe refreshing the list don't get cleared and the refresh item is shown in the bottom of the list – Saksham Khurana Mar 11 '22 at 11:29
  • Can you update the question with the code where you add your items to list and handling the loading states and swipe refresh? We are kinda sticker here. – Jan Bína Mar 11 '22 at 22:13
  • So, in the first example from docs, pager is parameter of the composable function and there is nothing about how it's created. In the article there is `val movies: Flow> = Pager` in the viewmodel, which is exactly what I suggested. So I'm sorry but I don't see what you are talking about here. – Jan Bína Mar 11 '22 at 22:17
  • updated the question with list states... first thing if you pass pager as parameter (the list always refreshes on navigate, try to mock it you will see)... in second example (yes if I do that) same issue with swipe refresh (on refresh list not getting clear & load item gets inserted in the end) – Saksham Khurana Mar 12 '22 at 02:26
  • edited the answer with complete code that should do what you want (I hope) – Jan Bína Mar 12 '22 at 11:29
  • I am trying to something similar. but want to invalidate paging data on external call like onResume() or search/filter. What's the best practice to invalidate paging data – iamdeowanshi Aug 05 '22 at 00:17