Items are hidden from RecyclerView after update till user scroll - android

I have a fragment [DISPLAY SCREEN] with RecyclerView After user click on item of it, app open screen [EDIT SCREEN] to allow user edit that itme.
when user finish update i close fragment [EDIT SCREEN] and back to [DISPLAY SCREEN] and make new request to api to get data again
i checked thread of update method which accept data from api and thread was main
private fun updateScreen(data: List<GymPackage>) {
"updateScreen size ${data.size} pageNumber $pageNumber , thread num ${Thread.currentThread().name}".log(mTag)
if (pageNumber++ == 1)
packagesAdapter.clear()
packagesAdapter.append(data)
("updateScreen size ${data.size} pageNumber $pageNumber ,, adapter ${packagesAdapter.itemCount}, ${packagesAdapter.getItemViewType(0)}" +
",${(packagesRecyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()}").log(mTag)
/**
* after get first page sometimes items not being shown so scroll to first item
*/
packagesRecyclerView.scrollBy(0 , 50)
loadMore = data.size == pageSize
packagesAdapter.loadingProgressBar = data.size == pageSize
tvNoData?.visibility = (packagesAdapter.itemCount == 0).gotViewVisiblity(true)
}
Output:
first two lines are with when [DISPLAY SCREEN] get list of items,
thread is main
n items inside adapter of RecyclerView -> 5
0 is index of first visible item in RecyclerView
second two lines after return from [EDIT SCREEN]
thread is main
n items inside adapter of RecyclerView -> 5
-1 is index of first visible item in RecyclerView --> so user do not see any items
when user scroll he sees items normally.
I tried to make rv scroll using
packagesRecyclerView.scrollBy(0 , 50)
also
packagesRecyclerView.scrollToPosition(0)
both with no results
Update 1
tried these
packagesAdapter.notifyDataSetChanged()
packagesRecyclerView.performClick()
with no results
Update 2
with beta7 Google said it provides fixes to
Nested scroll view issues in MotionLayout
Transition listener issues with MotionLayout
Memory leak in MotionLayout
RecyclerView performances
Group visibility
Padding issues
after I update lib to, app works fine, but motion has other issues with padding
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'

Return to beta4
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
and use
motion.transitionToStart()

Related

Incorrect relayouting of recycler view elements

I'm developing android messenger application and using a RecyclerView for showing messages. I have a button that loads all messages from server and scroll down to the last one by clicked on it. However, after clicking on this button I see only half of the last message's content. In addition, when I scroll a little up and click on this button for the second time I don't have a problem and see all content of last message.
Code of setuping the button.
private fun setupToLastMessagesButton() {
binding.toLastMessagesButton.setOnClickListener {
// Show progress bar while messages is loading
binding.mainProgressBar.visibility = View.VISIBLE
// Tell service's thread to load messages by using handler and post mechanism
getServiceHandler().post {
val result = getService().getLastMessages()
// Tell main thread to update recycler view and scroll down
mainHandler.post {
// Finish showing progress bar
binding.mainProgressBar.visibility = View.GONE
// Notify adapter
getAdapter().notifyItemRangeInserted(result.first, result.second)
// Scroll to last element
binding.messagesView.scrollToPosition(getAdapter().itemCount - 1)
}
}
}
}
RecyclerView .xml code
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/messagesView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="#+id/recyclerViewProgressBar"
app:layout_constraintTop_toTopOf="parent" />
Screenshot shows my problem. You can see that last element have incorrect layout and you can't see a text of message.
I guess that RecyclerView doesn't have time to complete relayouting before calling 'binding.messagesView.scrollToPosition(getAdapter().itemCount - 1)'. I tried to call this function after some delay, but nothing changed. Is there a mechanism to wait a moment when relayoting is completed?
Screenshot shows behaviour I'm expecting.
you should call scrollToPosition in yet another post, give a moment for RecyclerView for obtaining data and measure/draw (in memory, first frame) new item.
binding.messagesView.post {
binding.messagesView.scrollToPosition(getAdapter().itemCount - 1)
}
currently it is scrolling to half of this new item probably because it isn't drawn yet and RecyclerView "thinks" that this item is smaller (so scrolling a bit to small amount of pixels, not to the end). I bet your "clear" item/layout (not filled with msg data) doesn't contain this gray area with numbers, so RecyclerView scrolls only to always visible username-row

Android ConcatAdapter scrolls to the top when prepend elements but keep the position when append

I am making a movie list with RecyclerView and ConcatAdapter.
The ConcatAdapter consists of three different adapters, HeaderAdapter, MovieAdapter and FooterAdapter.
HeaderAdapter - displays either of progress bar, load error, or normal header. Its ViewType is different from the one for MovieAdapter.
MovieAdapter - It extends ListAdapter and displays movies with paging.
FooterAdapter - displays either of a progress bar, load error, or normal footer. Its ViewType is different from the one for MovieAdapter.
They all set up like this
recyclerView.adapter = ConcatAdapter(
ConcatAdapter.Config.Builder().setIsolateViewTypes(false).build(),
headerAdapter,
movieAdapter,
footerAdapter
)
The issue is that the scroll position jumps to the top when users scroll up, reach to the top, load more movies and prepend the newly fetched movies with ListAdapter's submitList() But I want to keep the current position not jumps to the top.
Let me break down the problem scenario
I have 50 movies {100, 101, .... 150} in the RecyclerView and they are already displayed
I scroll all the way up to the top and now see the progress bar that is displayed by the HeaderAdapter
I load 50 more movies {49, 50, 51, ... 99} and prepend them to the list as followings
val temp = mutableListOf<Value>()
val newMovies = loadMoreMoviesFrom(lastMovieId)
temp.addAll(newMovies)
// temp = {49, 50, 51, .... 99}
temp.addAll(currentList)
// temp = {49, 50, 51, .... , 99, 100, 101, ..... 150}
submitList(temp)
Then the list does not keep the current position but jumps to the top which shows the progress bar (Header) and 49, 50 as per the gif below. But I want to keep the position as it is.
But if I APPEND new movies in the same way as I did for prepeding, it actaully works as expected
Let's say I have 50 movies {0, 1, 2, ... 49}, scroll all the way down to the bottom, see the progress bar displayed by FooterAdapter, load more 50 movies {50, 51, .... 100}, append them to the list and submit. Then the list keeps the current position as per the gif below
I know I can use Android paging library 3 but I do have some reason why I want to paginate on my own and I don't want to explain it here because it's not the question.
The method to load another 50 chunks, prepend and submit is called from RecyclerView.OnScrollListener() when the scroll position reaches the end. I just took the part only and put it here for easy understanding.
So why the list jumps to the top when I prepend but stays at the current position when I append. How can I prevent the list from jumping to the top when I prepend?
Thank you guys and hope the gifs are of any help
As i understand, your response is a chunk of 50 items, and every time you scroll down, you request another chunk of 50 item
simply if you make it manually, it won't be efficient whatever the workaround that you will do, so google offers a library it's called pagination which matches your case
check the documentation of paging library, there is a codelab that explains every step you need
and if you want to make it manually, you can trace the scroll position of the recycler view by implementing the addOnScrollListener method, and every time you request a new chunk, scroll to the last position but I don't recommend that way, I prefer to use a pagination
\ in class level
var position = -1 //inital value in first chuck
var totalScrollAfter = 0
in onscollChangedLisener method
position = item.posion
you can get it from the adapter of recycler view, last position which is here 49, and it will be 98 ,... and so on
totalScrollAfter = recyclerView!!.computeVerticalScrollOffset()
// when you request another chunk, and the request is ready to be submitted using
if (postion > -1) { recyclerView.scrollBy(0, totalScrollAfter) }

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.

RecyclerView does not preserve scroll position when data changed

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

RecyclerView and Adapter data updates

This is a question about RecyclerView internal behavior for someone that knows its mechanics or is willing to dig into the source code. I’d like an answer backed up by references to the source.
Original question
(scroll down to ‘In other words’ for a more focused question)
I need to understand how notify* actions (for example, notifyItemInserted()) are enqueued. Imagine I have an adapter backed up by this list:
ArrayList<String> list = Arrays.asList("one", "three", "four");
I want to add the values zero and two, that are missing.
Example 1
list.add(1, "two");
// notify the view
adapter.notifyItemInserted(1);
// Seconds later, I go on with zero
list.add(0, "zero");
// notify the view
adapter.notifyItemInserted(0);
This is pretty straightforward and clear, nothing to tell.
Example 2
But what if the two actions are very close to each other, and there’s no layout pass in between?
list.add(1, "two");
list.add(0, "zero”);
What should I do now?
adapter.notifyItemInserted(1);
adapter.notifyItemInserted(0);
Or maybe
adapter.notifyItemInserted(2);
adapter.notifyItemInserted(0);
? From the adapter perspective, the list immediately switched from one, three, four to zero, one, two, three, four so the second option seems more reasonable.
Example 3
list.add(0, “zero”);
adapter.notifyItemInserted(0);
list.add(2, “two”);
adapter.notifyItemInserted(...)
What about it now? 1 or 2 ? The list was updated immediately after, but I am sure there was no layout pass in between.
Question
You got the main issue, and I want to know how should I behave in these situations. The real case is that I have multiple asynchronous tasks ending up in an insert() method. I can enqueue their operations, but:
I don’t want to do that if there’s already an internal queue, and there surely is
I don’t know what happens if two actions happen without a layout pass in between, see Example 3.
In other words
To update recycler, 4 actions must happen:
I actually alter the data model (e.g. insert something into the backing array)
I call adapter.notify*()
Recycler receives the call
Recycler performs the action (e.g. calls getItem*() and onBind() on the adapter) and lays out the change.
It’s easy to understand this when there’s no concurrency, and they happen in sequence:
1. => 2. => 3. => 4. => (new update) 1. => 2. => 3. => 4. ...
Let’s see what happens between steps.
Between 1. and 2.: I would say it is the developer responsibility to call notify() immediately after having altered the data. That’s OK.
Between 2. and 3.: This happens immediately, no issue here.
Between 3. and 4.: This does not happen immediately! AFAIK. So it perfectly possible that a new update (steps 1 and 2) comes between steps 3 and 4 of the previous update.
I want to understand what happens in this case.
How should we behave?
Should I ensure that step 4 of the previous update did took place before inserting new stuff? If so how?
I thought about similar questions before, and I decided:
If I want to insert more than 1 item directly to end of list and
want to get a animation for all, I should:
list.add("0");
list.add("1");
adapter.notifyItemRangeInserted(5, 2); // Suppose there were 5 items before so "0" has index of 5 and we want to insert 2 items.
If I want to insert more than 1 item directly to end of list, but
want to get separated animation for each inserted item, I should:
list.add("0");
list.add("1");
adapter.notifyItemInserted(0);
mRecyclerView.postDelayed(new Runnable() {
#Override
public void run() {
// before this happens, Be careful to call other notify* methods. Never call notifyDataSetChanged.
adapter.notifyItemInserted(1);
}
}, mRecyclerView.getItemAnimator().getAddDuration());
If I want to insert more than 1 item to different position of list,
similar as 2.
Hope this can help.
So lets start from little intro to RecyclerView works with notify items. And works pretty simple with other list of saved ViewGroup items (ListView for ex.)
RecyclerView has Queue of View Items which already drawn. And doesn't know about any your updates, without calling notify(...) methods. When you added new Items and notify RecyclerView, it starts cycle for checking all Views one by one.
RecyclerView contains and drawn next objects
View view-0 (position 0), view-1 (position 1), View-2 (position 2)
// Here is changes after updating
You added Item View view-new into (position 1) and Notify
RecyclerView starts loop to check changes
RecyclerView received unmodified view-0(position-0) and left them;
RecyclerView found new item view-new(position 1)
RecyclerView removing old item view-1(position 1)
RecyclerView drawing new item view-new(position 1)
// In RecyclerView queue in position-2 was item view-2,
// But now we replacing previous item to this position
RecyclerView found new item view-1 (new position-2)
RecyclerView removing old item view-2(position 2)
RecyclerView drawing new item view-1(position 2)
// And again same behavior
RecyclerView found new item view-3 (new position-3)
RecyclerView drawing new item view-1(position 2)
// And after all changes new RecyclerView would be
RecyclerView contains and drawn next objects
View view-0 (position 0), view-new (position 1) view-1 (position 2), View-2 (position 3)
It's just main flow of working notify functions, but what should know all this actions happens on UI Thread, Main Thread, even you can calling updating from Async Tasks. And answering you 2 Question - You can call Notify to the RecyclerView as much as you want, and make sure, you action would be on the correct Queue.
RecyclerView works correct in any usage, more complicated questions would be to your Adapter work. First of all, you need to synchronize you Adapter action, like adding removing items, and totally refuse of index usage. For example, it's would be better for your Example 3
Item firstItem = new Item(0, “zero”);
list.add(firstItem);
adapter.notifyItemInserted(list.indexOf(firstItem));
//Other action...
Item nextItem = new Item(2, “two”);
list.add(nextItem);
adapter.notifyItemInserted(list.indexOf(nextItem))
//Other actions
UPDATE |
Related to RecyclerView.Adapter Doc, where you can see functions same with notifyDataSetChanged(). And where this RecyclerView.Adapter invokes child items with android.database.Observable extensions, see more About Observable. Access to this Observable Holder is synchronized, until View Element in RecyclerView release usage.
See also RecyclerView from support library version 25.0 Lines 9934 - 9988;
It should not be a problem if you make multiple updates between layout passes. The RecyclerView is designed to handle (and optimize) this case :
RecyclerView introduces an additional level of abstraction between the
RecyclerView.Adapter and RecyclerView.LayoutManager to be able to
detect data set changes in batches during a layout calculation. [...]
There are two types of position related methods in RecyclerView:
layout position: Position of an item in the latest layout calculation. This is the position from the LayoutManager's
perspective.
adapter position: Position of an item in the adapter. This is the position from the Adapter's perspective.
These two positions are the same except the time between dispatching
adapter.notify* events and calculating the updated layout.
In your case the steps are :
You update the data layer
You call adapter.notify*()
The recyclerview record the change (in AdapterHelper.mPendingUpdates if I understand the code correctly). This change will be reflected in ViewHolder.getAdapterPosition(), but not yet in ViewHolder.getLayoutPosition().
At some point the recyclerView apply the recorded changes, basically it reconcile the layout's point of view with the adapter's point of view. It seems that this can happen before the layout pass.
The 1., 2., 3. sequence can happen any number of times as long as 2. immediately follows 1. (and both happen on the main thread).
(1. => 2. => 3.) ... (1. => 2. => 3.) ... 4.
Item firstItem = new Item(0, “zero”);
list.add(firstItem);
adapter.notifyItemInserted(list.indexOf(firstItem));
//Other action...
Item nextItem = new Item(2, “two”);
list.add(nextItem);
adapter.notifyItemInserted(list.indexOf(nextItem))

Categories

Resources