Reputation: 2009
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
Reputation: 3469
put your refersh logic in onResume() instead of onCreate(), it's an issue of lifecycle management.
Upvotes: 1