Alvin Dizon
Alvin Dizon

Reputation: 2009

Paging3: calling refresh() on adapter doesn't trigger refresh when returning from other fragment

I am using the latest Paging3 library for my app, which has a gallery screen displaying a list of photos, and a details screen showing more options and info on a photo. I have setup the gallery to fetch a list of photos in my Fragment's onCreate:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // display all photos, sorted by latest
        viewModel.getAllPhotos()
    }

If successful, the photos are passed to the adapter via submitList, and if the user pulls down the gallery screen, it should trigger a refresh, so I've setup a refreshListener accordingly. I do this on onViewCreated(note that I use ViewBinding):

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding = FragmentGalleryBinding.bind(view)

        viewLifecycleOwner.lifecycle.addObserver(viewModel)

        setupGallery()

        setupRetryButton()
    }

    private fun setupGallery() {
        // Add a click listener for each list item
        adapter = GalleryAdapter{ photo ->
            photo.id.let {
                findNavController().navigate(GalleryFragmentDirections.detailsAction(it))
            }
        }

        viewModel.uiState?.observe(viewLifecycleOwner, {
            binding?.swipeLayout?.isRefreshing = false
            adapter.submitData(lifecycle, it)
        })

        binding?.apply {
            // Apply the following settings to our recyclerview
            list.adapter = adapter.withLoadStateHeaderAndFooter(
                header = RetryAdapter {
                    adapter.retry()
                },
                footer = RetryAdapter {
                    adapter.retry()
                }
            )

            // Add a listener for the current state of paging
            adapter.addLoadStateListener { loadState ->
                Log.d("GalleryFragment", "LoadState: " + loadState.source.refresh.toString())
                // Only show the list if refresh succeeds.
                list.isVisible = loadState.source.refresh is LoadState.NotLoading
                // do not show SwipeRefreshLayout's progress indicator if LoadState is NotLoading
                swipeLayout.isRefreshing = loadState.source.refresh !is LoadState.NotLoading
                // Show loading spinner during initial load or refresh.
                progressBar.isVisible = loadState.source.refresh is LoadState.Loading && !swipeLayout.isRefreshing
                // Show the retry state if initial load or refresh fails.
                retryButton.isVisible = loadState.source.refresh is LoadState.Error

                val errorState = loadState.source.append as? LoadState.Error
                    ?: loadState.source.prepend as? LoadState.Error
                    ?: loadState.append as? LoadState.Error
                    ?: loadState.prepend as? LoadState.Error
                errorState?.let {
                    swipeLayout.isRefreshing = false
                    Snackbar.make(requireView(),
                        "\uD83D\uDE28 Wooops ${it.error}",
                        Snackbar.LENGTH_LONG).show()
                }
            }

            swipeLayout.apply {
                setOnRefreshListener {
                    isRefreshing = true
                    adapter.refresh()
                }
            }
        }

On first load, the pulling down the layout triggers the refresh successfully. However a problem arises after I navigate to the details screen. From the details screen, pressing the back button returns the user to the gallery. If the users pulls the layout, the progress indicator appears but adapter.refresh() does not happen. I am at a loss as to how to debug this.

For reference, here is how my ViewModel that's in charge of fetching photos looks like:

class GalleryViewModel(private val getAllPhotosUseCase: GetAllPhotosUseCase): BaseViewModel() {

    private val _uiState = MutableLiveData<PagingData<UnsplashPhoto>>()
    val uiState: LiveData<PagingData<UnsplashPhoto>>? get() = _uiState

    fun getAllPhotos() {
        compositeDisposable += getAllPhotosUseCase.getAllPhotos()
            .cachedIn(viewModelScope)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeBy(
                onNext = { _uiState.value = it },
                onError = {
                    it.printStackTrace()
                }
            )
    }

}

The GetAllPhotosUseCase forwards the getAllPhotos call to a Repository implementation that contains the following:

class UnsplashRepoImpl(private val unsplashApi: UnsplashApi): UnsplashRepo {

    override fun getAllPhotos(): Observable<PagingData<UnsplashPhoto>> = Pager(
        config = PagingConfig(Const.PAGE_SIZE),
        remoteMediator = null,
        // Always create a new UnsplashPagingSource object. Failure to do so would result in a
        // IllegalStateException when adapter.refresh() is called--
        // Exception message states that the same PagingSource was used as the prev request,
        // and a new PagingSource is required
        pagingSourceFactory = { UnsplashPagingSource(unsplashApi) }
    ).observable

....
}

My RxPagingSource is setup like this:

class UnsplashPagingSource (private val unsplashApi: UnsplashApi)
    : RxPagingSource<Int, UnsplashPhoto>(){

    override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, UnsplashPhoto>> {
        val id = params.key ?: Const.PAGE_NUM
        return unsplashApi.getAllPhotos(id, Const.PAGE_SIZE, "latest")
        .subscribeOn(Schedulers.io())
            .map { response ->
                response.map { it.toUnsplashPhoto() }
            }
            .map<LoadResult<Int, UnsplashPhoto>> { item ->
                LoadResult.Page(
                    data = item,
                    prevKey = if (id == Const.PAGE_NUM) null else id - 1,
                    nextKey =  if (item.isEmpty()) null else id + 1
                )
            }
            .onErrorReturn { e -> LoadResult.Error(e) }
    }
}

Can anyone point me in the right direction with this?

EDIT: As Jay Dangar has said, moving viewModel.getAllPhotos() to onResume would make the call to adapter.refresh() trigger successfully. However, I do not want to fetch all photos every time I navigate from the details screen to the gallery. To avoid this, instead of calling adapter.refresh() when the layout is pulled, I just call viewModel.getAllPhotos() instead.

I still don't understand why the accepted answer works, but I assume that adapter.refresh() only works when a new PagingSource is created or something.

Upvotes: 2

Views: 2399

Answers (1)

Jay Dangar
Jay Dangar

Reputation: 3469

put your refersh logic in onResume() instead of onCreate(), it's an issue of lifecycle management.

Upvotes: 1

Related Questions