Problem Statement: Using Paging3 library and need to show loading information in header when user swipes to refresh. I followed the sample provided in the here.
Solution: Tried adding refresh header in LoadStateAdapter for with using pagingAdapter.withLoadStateHeaderAndFooter().
messagesRecyclerView.adapter = messagesAdapter.withLoadStateHeaderAndFooter(
header = MessageHeaderStateAdapter(messagesAdapter),
footer = MessageLoadStateAdapter()
)
But it does not shows the header until I swipe it 5-6 times in quick successions.
Footer, on other hand, shows just fine with loading more and error layouts based on LoadState.
Error State
Loading More State
Any pointers as to how this can be achieved are much appreciated.
withLoadStateHeader just returns a ConcatAdapter that listens to PREPEND state. If you want the header to show up during REFRESH you can basically copy the code out of that helper, but instead update LoadStateAdapter based on REFRESH LoadState using a listener on your PagingDataAdapter
val header = MyLoadStateHeader()
adapter.addLoadStateListener { loadStates ->
header.loadState = loadStates.refresh
}
recyclerView.adapter = ConcatAdapter(header, this)
Related
I am loading items from API using the Paging 3 library. I would like to add an item to a specific position in the recyclerview.
If I am using normal adapter I would just use
mArrayList.add(position, item);
notifyItemInserted(position);
But I am using the PagingDataAdapter and I have tried this
val newPostResponse = response?.responseBody
homeAdapter.snapshot().items.toMutableList().add(0, newPostResponse)
homeAdapter.notifyItemInserted(0)
I have also tried
val newPostResponse = response?.responseBody
homeAdapter.snapshot().items.toMutableList().add(0, newPostResponse)
homeAdapter.notifyDataSetChanged()
None of these is working. Someone help me know the right way to do it.
This seems to be a duplicate of How to update single item using Paging 3 library.
You must go through the .invalidate() loop to maintain a single source of truth and make Paging aware of your local changes. This is currently the only supported way to add an item to Paging.
There is a situation where I have to scroll the recyclerview to last position. The problem starts where I am using Paging 3. I don't know what is the proper way to do it every time I submit data to adapter. by the way I have to say that I do not submit data once, but for example every time user submits a new comment to backend I have to get the new list from server and submit it.
This is what I tried. that's how I observe data:
private fun observeComments() {
exploreViewModel.commentsLiveData.observe(viewLifecycleOwner) { comments ->
commentsAdapter.submitData(viewLifecycleOwner.lifecycle, comments)
}
}
And this is how I tried to scroll the recyclreview to last item:
commentsAdapter.addLoadStateListener { loadStates ->
if(loadStates.refresh.endOfPaginationReached){
if(commentsAdapter.itemCount>1)
binding.rvComments.smoothScrollToPosition(commentsAdapter.itemCount-1)
}
}
I also set app:stackFromEnd="true" to recyclerview but this is not useful. whenever I submit a new comment and get the list, recyclerview scrolls to the middle, I thinks it scrolls to the first pages end. but I want it to be scrolled to the end of the list.
Any help or suggestion would be appreciated.
You can implement PagingSource.getRefreshKey() to control the key passed to .load() after invalidation and set initialKey in Pager() to control the key passed to initial call of .load().
Since your list is paginated, scrolling to the end doesn't work because you would need to sequentially load all pages until you get to your desired position, so instead the better strategy is to start loading from where you want the user's scroll position to start, then let paging prepend pages as they scroll back up.
I have a list of articles and when new articles appear on the server, I display the button informing user about that. When user clicks this button I want to load newest articles and add them to beginning of the list. While new articles are loading, I am displaying placeholder items (view skeletons of article item without data) for them in the RecyclerView and I scroll to top of the list so newest articles are visible.
The problem is that when new articles are fetched and they replace placeholders, the RecyclerView is not scrolled to the top but instead is scrolled on previous data and I have to manually scroll to be at the top of the list.
This has probably something to do with attempt of RecyclerView to keep user scrolled where he was before, but in this case, user was not here.
The workaround around this is either disable animations on RecyclerView or to not make diff of the data using DiffUtil but call notifyDatasetChanged.
I went with the second approach because I want to have animations
I have created a showcase repository demonstrating this problem. There are two Activities, one with vanilla RecyclerView and RecyclerView.Adapter and second one using epoxy library.
The code for the whole class is pretty complicated and I am not sure which is more relevant so this is how the whole Activity looks. She simulation of loading new articles
private fun simulateFetchNewArticles() {
scope.launch {
val newArticlesCount = 10
adapter.items = (0 until newArticlesCount).map { Item.Placeholder("Placeholder $it") } + adapter.items
delay(100)
recyclerView.smoothScrollToPosition(0)
delay(5000)
// Uncomment for workaround fix
// adapter.items = emptyList()
adapter.items = (0 until newArticlesCount).map { Item.Article("New article $it") } + adapter.items.drop(newArticlesCount)
}
}
I can get a total number of records from the server, so I am using PositionalDataSource for Network call. Everything works well but one problem: even if I don't scroll, the paging library continues fetching all the data from the server. How to solve this problem?
Using: androidx.paging:paging-runtime-ktx:2.1.1
Example:
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Doctor>) {
fetchDataFromApi(params.requestedStartPosition, params.pageSize) { list, totalCount ->
callback.onResult(list, 0, totalCount)
}
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Doctor>) {
fetchDataFromApi(params.startPosition, params.loadSize) { list, totalCount ->
callback.onResult(list)
}
}
PagedList.Config.Builder()
.setPageSize(14)
.setPrefetchDistance(4)
.setInitialLoadSizeHint(14)
.setEnablePlaceholders(true)
.build()
It sounds like a similar issue I spend the last two days looking into. For me it turned out, that I had wrapped my RecyclerView inside a NestedScrollView.
However, the pagination library doesn't have built-in support for that according to this issue on Google tracker.
when you put recyclerview inside an infinite scrolling parent, it will layout all of its children because the parent provides infinite dimensions.
Basically the NestedScrollView continues triggering fetching more data as if the screen is as long as the complete list
Two options if this is the case for your case too:
Change your layout setup to not need the NestedScrollView
Here is a code example gist that helped me create a custom [SmartNestedScrollView]. Note that you have to set android:fillViewport="true" as well on this custom container and update the RecyclerView to have a height of wrap_content and remove the nestedScrollingEnabled flag from it.
Setup :
We are using PagedListEpoxyController, RxPagedListBuilder, ItemKeyedDataSource with a runtime Mockgenerator which just creates a user with respect to the index of data that is generated, for eg. user1, user100, user500 etc.
Paging configuration is :
val pageSize = 50
val initialLoadSizeHint = 80
val prefetchDistance = 16
Expected behaviour
Whenever we scroll down to the 400th item and come back to the 300th item and invalidate the list by clicking Follow/Unfollow the UI jumps. When the button is clicked the PagedList is invalidated like this:
followers.dataSource.invalidate()
loadInitial() inside ItemKeyedDataSource should be called with the value of params.key so we can get the same page that was previously displayed to be re-displayed. For example if user300 was visible in the screen, that item requestedInitialKey should be in the range of [user221,user280].
Where is the issue?
Paging Library tracks of the last item that was fetched in the pagedList before invalidation but does not provide a way to know which was the last item/range that was visible in the screen just before invalidation.
After invalidating the pagedList, The first callback which gets called after pagedList invalidation is loadInitial(params : LoadInitialParams, callback : LoadInitialCallback) with the params only holds the information about pageSize and placeholder along with requestedInitialKey as last item that was fetched before invalidation which is very misleading. Why, after invalidation a pagedList does request the last item to be fetched in the list as the requestedInitialKey / item in the firstPage rather than the last item that was displayed before invalidation?. Sorry if I have totally misunderstood this thing but couldn't really make sense of it.
Question :
I wish pagedList to start fetching the first page in new PagedList after invalidation from the previous scroll Location rather than the last item in the previous pagedList.
But if PagedList is not supposed to work this way then How can I know any of the item/index that was previously visible in the screen before invalidation?
If I can't do that currently either, what is the recommended way to achieve this requirement?
This issue can be found here :
To reproduce : Scroll down far enough (like 60th item) and press "Refresh" you will see the jump in the list.
Link to github issue
The way we solved this issue is, UI only listens from Room Database as a single source of truth. This issue is not present in that setup. But when we use ItemKeyedDatasource, this issue is persistent even until now.