RecyclerView does not preserve scroll position when data changed - android

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)
}
}

Related

Show Header when Swipe to Refresh with Jetpack Paging3 Android

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)

Why recyclerview with paging not working properly?

Hy,
So, I have 2 fragments, the first one is a browser fragment, the second one is a detail fragment.
I noticed that, if I scroll down a lot, for example to the 200th item, and I go to the item details fragment, and after that I navigate back, the recyclerview automtically scrolls up herself for the 60th item.
After that I saw the code, and I noticed that, I use paging library, and in the config I set up the page size to 30, and the InitialLoadSizeHint to 2*pagesize which is 60.
So I think that, this is cousing the problem, when I navigate back, the InitialLoadSizeHint is 60 but the actual recycler view position is 200.
If I increase the pagesize to 100, after back navigation the recycler view jumps for the 200th item.
Additional information, I use LiveData to pupulate data from database.
How can I fix that? What did I wrong?
**Pagedlist config**
val config = PagedList.Config.Builder()
.setEnablePlaceholders(true)
.setPageSize(model.pageSize)
.setPrefetchDistance(model.pageSize)
.setInitialLoadSizeHint(model.pageSize * 2).build()
**List observer**
viewModel.listData.observe(viewLifecycleOwner, Observer {
Timber.d("submitting new list")
baseBinding.get()?.progressBar?.visibility = View.GONE
adapter.get()?.submitList(it) {
setAppBarScrolling()
}
baseBinding.get()?.productBrowserProducts?.scrollToPosition(prefManager.RECYCLERVIEW_LAST_POSITION)
})
Unfortunately I made a big mistake, because inside the fragment onCreateView method I triggered my filter, and because of this, the observer is called, and although the viewModell didn't destroyed, still reloaded the data. So I deleted this line, and problem solved.

What is the proper way to scroll to end of recyclerview in paging 3

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.

How to scroll to the bottom when using RecyclerView, PagedListAdater and Room?

Generally we can use the following code to go to the bottom if all data has been loaded into memory.
recyclerView.scrollToPosition(YourArraylist.size()-1);
But when using PagedListAdater and Room, the entity of the last item wasn't loaded at all. So when calling recyclerView.scrollToPosition, I can scroll with the scroll bar, but the data doesn't show. On my screen, I saw some words like "ABV.."; and if I scroll up/down manually, these view might be able to show correct.
BTW, I also tried ((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPositionWithOffset(index,0);, it doesn't work either.
The associated code is as below,
DataSource.Factory<Integer, ContactWithSectionView> dsFactory = contactDao.listAll();
concertList = new LivePagedListBuilder<>(dsFactory, 5)
.build();
concertList.observe((LifecycleOwner)context,(pagedList) -> {
this.adapter.submitList(pagedList);
});
#Dao
public interface ContactDao {
#Query("select * from ContactWithSectionView")
DataSource.Factory<Integer, ContactWithSectionView> listAll();
}
I tried to call concertList.getValue().loadAround(index) to load data before call recyclerView.scrollToPosition(int), it didn't work either. -- index is the position where I want to move to.
Finally, I gave up. didn't use the PagedListAdapter + Room, and load all items to memory in one time.

Setting RecyclerViews itemAnimator to null does not remove animations

My outer RecyclerView crashes either with
IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true...
or
IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
Like the title suggests I have an RecyclerView in the list item layout of the first RecyclerView. This layout is used to display messages and the
inner RecyclerView to display attachments that come with the message. The inner RecyclerViews visibility is set to either GONE or VISIBLE depending whether the message has any attachments or not. The simplified outer list item layout looks like this
ConstraintLayout
TextView
TextView
TextView
RecyclerView
And the part of the adapter that handles the inner RecyclerView looks like this
private fun bindFiles(message: Message?) = with(itemView) {
if (message != null && message.attachments.isNotEmpty())
{
sent_message_attachments.setAsVisible()
sent_message_attachments.layoutManager = GridLayoutManager(this.context,Math.min(message.attachments.size,3))
sent_message_attachments.adapter = AttachmentAdapter(message.attachments)
sent_message_attachments.itemAnimator = null
sent_message_attachments.setHasFixedSize(true)
}
else{
sent_message_attachments.setAsGone()
sent_message_attachments.adapter = null
sent_message_attachments.layoutManager = null
}
}
The bug has something to do with the way I fetch the attachments in the inner adapter since once I disable the part that start the download process, everything is fine. There's no problem when loading images from the device, but once I start the download process, everything goes to hell. This is the part that handles images and kicks off the download process in the inner adapter. I have functions for videos and for other file types that are pretty much the same exact thing but use slightly different layout.
private fun bindImage(item: HFile?) = with(itemView) {
if (item != null)
{
if (item.isOnDevice && !item.path.isNullOrEmpty())
{
if (item.isGif)
{
attachment_image.displayGif(File(item.path))
}
else
{
attachment_image.displayImage(File(item.path))
}
}
else
{
//TODO: Add option to load images manually
FileHandler(item.id).downloadFileAsObservable(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
//TODO: Show download process
},
{ error ->
error.printStackTrace()
//TODO: Enable manual retry
},
{ notifyItemChanged(adapterPosition)} //onComplete
)
}
}
}
I use the same structure as above in my DiscussionListAdapter to load discussion portraits (profile pictures etc.) and it does not have the same issue.
These are the extensions functions used to inflate the viewHolders and to display the images
fun ViewGroup.inflate(layoutRes: Int): View
{
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}
fun ImageView.displayGif(file:File){
GlideApp.with(context).asGif().load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
fun ImageView.displayImage(file:File){
GlideApp.with(context).load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
I've been on this for the past couple of days and just can't get my head around it. Any help in any direction is greatly appreciated. I know my explanations can be a bit all over the place so just ask for clarification when needed :)
UPDATE
I have now been able to produce this with a GridLayout as well as with RecyclerView. It's safe to assume that the nested RecyclerViews were not the culprit here. I even tried to ditch the Rx-piece that handled loading the images and created an IntentService for the process, but the same crashes still occur.
With GridLayout I mean that instead of having another adapter to populate the nested RecyclerView I use only one adapter to populate the message and to inflate and populate views for the attachments as well and to attach those views to the nested GridLayout.
The crash happens when I start to download a file and then scroll the view, that is supposed to show the downloaded file, out of the screen. That view should get recycled but for some reason the download process (which in my test cases only takes around 100ms-400ms) causes the app to throw one of the two errors mentioned in the original question. It might be worth noting that I'm using Realm and the adapter takes in a RealmResults<Message> list as it's dataset. My presenter looks for changes in the list and then notifies the adapter when needed (changed due to the implementation of IntentService).
This is how I'm capable to reproduce this time and time again:
Open a discussion that has messages with attachments
Start to scroll upwards for more messages
Pass a message with an attachment and scroll it off screen while it's still loading
Crash
There is no crash if I stop and wait for the download to complete and everything works as intended. The image/video/file gets updated with a proper thumbnail and the app wont crash if I scroll that out of view.
UPDATE 2
I tried swapping the nested ViewGroup for a single ImageView just to see is the problem within the nestedness. Lo and behold! It still crashes. Now I'm really confused, since the DiscussionListAdapter I mentioned before has the same exact thing in it and that one works like a charm... My search continues. I hope someone, some day will benefit from my agony.
UPDATE 3
I started to log the parent of every ViewHolder in the onBindViewHolder() function. Like expected I got nulls after nulls after nulls, before the app crashed and spew this out.
04-26 21:54:50.718 27075-27075/com.hailer.hailer.dev D/MsgAdapter: Parent of ViewHolder: android.view.ViewOverlay$OverlayViewGroup{82a9fbc V.E...... .......D 0,0-1440,2168}
There's a method to my madness after all! But this just poses more questions. Why is ViewOverlay used here? As a part of RecyclerView or as a part of the dark magicians plans to deprive me of my sanity?
Sidenote
I went digging into RecyclerViews code to check if I could find a reason for the ViewOverlaymystery. I found out that RecyclerView calls the adapters onCreateViewHolder() function only twice. Both times providing itself as the parent argument for the function. So no luck there... What the hell can cause the item view to have the ViewOverlay as it's parent? The parent is an immutable value, so the only way for the ViewOverlay to be set as the parent, is for something to construct a new ViewHolder and supply the ViewOverlay as the parent object.
UPDATE 4
Sometimes I amaze myself with my own stupidity. The ViewOverlay is used because the items are being animated. I didn't even consider this to be an option since I've set the itemAnimator for the RecyclerView as null, but for some odd reason that does not work. The items are still being animated and that is causing this whole charade. So what could be the cause of this? (How I chose to ignore the moving items, I do not know, but the animations became very clear when I forced the app to download same picture over and over again and the whole list went haywire.)
My DiscussionInstanceFragment contains the RecyclerView in question and a nested ConstraintLayout that in turn contains an EditText for user input and a send button.
val v = inflater.inflate(R.layout.fragment_discussion_instance, container, false)
val lm = LinearLayoutManager(context)
lm.reverseLayout = true
v.disc_instance_messages_list.layoutManager = lm
v.disc_instance_messages_list.itemAnimator = null
v.disc_instance_messages_list.adapter = mPresenter.messageAdapter
This is the piece that handles the initialization of the RecyclerView. I'm most definitely setting the itemAnimator as null, but the animations just wont stop! I've tried setting the animateLayoutChanges xml attribute on the root ConstraintLayout and on the RecyclerView but neither of them worked. It's worth mentioning that I also checked whether the RecyclerView had an itemAnimator in different states of the program, and every time I check the animator, it is null. So what is animating my RecyclerView?!
I have faced the same issue
Try this in your child RecyclerView it works for me
RecyclerView childRC = itemView.findViewById(R.id.cmol_childRC);
layoutManager = new LinearLayoutManager(context);
childRC.setItemAnimator(null);
childRC.setLayoutManager(layoutManager);
childRC.setNestedScrollingEnabled(false);
childRC.setHasFixedSize(true);
now set your Adapter like this
ArrayList<Model> childArryList = new ArrayList<>();
childArryList.addAll(arrayList.get(position).getArrayList());
ChildOrderAdapter adapter = new ChildOrderAdapter(context, childArryList);
holder.childRC.swapAdapter(adapter, true);
hope this helps
I finally figured out what was causing this. In my DiscussionInstanceView I have a small view that is animated into and out of view with ConstraintLayout keyframe animations. This view only shows the download progress of the chat history and is used only once, when the discussion is first opened. BUT since I had a call to hiding that view every time my dataset got updated, I was forcing the ConstraintLayout to fire of an animation sequence thus making everything animate during the dataset update. I just added a simple check whether I was downloading the history or not and this problem got fixed.

Categories

Resources