I have a ListView with an ArrayAdapter to back it. Updates to the list typically involve multiple item add/remove operations, followed by a sort operation. Often, the update just removes one item and adds an almost identical one, which eventually ends up in the same place as the item I removed.
How can I prevent the list view from “twitching” during these operations? Ideally, I would like to freeze all redraw operations, then add, remove and sort items, and only then “unfreeze” the UI, allowing it to be redrawn. Any way to achieve that?
ArrayAdapter#setNotifyOnChange() seems like a candidate for this. Use as follows:
myAdapter.setNotifyOnChange(false);
// add, remove and sort items here
myAdapter.notifyDataSetChanged();
Ensure nothing else in the update code calls ArrayAdapter#notifyDataSetChanged() (as doing so will trigger a redraw), and updates will be delayed until the transaction is complete.
EDIT: after trying this, there is still the occasional twitch, but that may also be caused by the fact that I scroll the selected list view item (back) into view after each update.
Related
I have a basic RecyclerView setup on a chat-like app and I have hit an issue with the item animations.
The project is making use of Room with Paging 3 and DiffUtils for the RecyclerView adapter, so this is all automated, but the core of the problem can be simplified to this:
When I send a new message, that message is added to the RecyclerView
here the adapter is triggering notifyItemInserted or notifyItemRangeInserted which causes the entire message list to shift up softly and the new message fades in after
I scroll the list to the bottom so the new added item becomes visible
When I receive a read status from the server I update the status of that message
here the adapter is triggering notifyItemChanged or notifyItemRangeChanged which has no default animations on its own, it just updates the item with the new information
All of this is working well on its own, but the problem is when I receive a status update from the server faster than the insert animation has a chance to finish. When that happens the notifyItemChanged or notifyItemRangeChanged kicks in and skips the animation initiated by notifyItemInserted or notifyItemRangeInserted. The list till shifts upwards, but the fade in no longer happens, instead the item is instantly made visible all the while the list is still shifting up, overlaying the item previously occupying that last position causing an ugly visual experience.
I can kinda "cheat" by delaying the step in 2. to engage after the animation is supposedly over, but then it introduces another visual issue if the user sends multiple messages quickly or receives them in the same fashion or in certain cases it just does not show any animation because the new item is loaded outside of the list and only scrolled after the animation time is elapsed, so this is not a solution.
first
second
In this example there are 2 recyclerviews set up with the same adapter slightly changed to make it easier to compare the issue in the same action.
The left recyclerview is not doing any update when an item is inserted, but it is the behavior I expect to display even if I update the item during the item insertion animation.
On the right recyclerview is the actual problem, as you can see new items are showing in full over the old ones before they have a chance to move out of the way.
The first example recording has scroll to bottom with no delay after the item is inserted, the second example has a delay that matches the insertion animation duration.
Reminder: this is just a manual example, the real application in my case is being done automatically via the integration I mentioned above, I am not the one in control of when the notifyItem* calls are made at any point.
How can I make sure the insert animation does not get interrupted even if I am updating the item data in the middle of the animation?
EDIT: I already searched for a solution in the questions posted before, but none are related to this one nor do the similar ones provide a solution to my problem.
I have a ListView/RecyclerView. Now, I want to add more information for each item and the information could be found in database according to the item value. How would you guy achieve it??
For the best practice, should I query database in the background thread? Should I cancel the query if user scrolling fast?
There is no "best practice" AFAIK, but the following is a short summary of your options:
Query on UI thread as the user scrolls
It is usually a very bad practice to execute queries on UI thread in general. It is a sin to do this in ListView.
That said, there are ORM libraries that perform lazy loading of nested objects on UI thread (e.g. GreenDAO). People use these libraries in applications that have ListViews and it even works alright for some of them.
I would strongly advice against this method.
Query of all items ahead of time
As #CommonsWare mentioned, for small to medium collections you can just load the entire dataset into memory on background thread and then bind it to the ListView. You will need to show some kind of progress indication while the data is being loaded.
The definition of "small" and "medium" is very vague, but I'd say that if you are sure that the dataset will not be larger than few MBs, then this method can work pretty well.
The drawback of this method is that the user will need to wait for the entire dataset to be loaded and bound to the ListView. Depending on the size of the dataset and the complexity of database scheme this might take a while.
Query a predefined number of items initially, and then perform additional queries as the user scrolls
This is the most complex scheme of all, but it is inevitable in some cases (e.g. "infinite list").
The idea is to get some predefined number of items into the ListView, and then track user's interactions in order to supply additional items.
For example, you can fetch 100 items initially, and then fetch additional 100 when the user scrolls to the end of the list.
An optimization of this scheme would be to fetch additional items before the user gets to the end of the list (let's say when he scrolled through 50 items). This allows to create a truly "infinite list" behavior.
Note that if your collection is very large, you will need to take care not only of adding a new items as the user scrolls, but also of removing "previous" items in order to avoid out of memory crash.
Query of additional information for items that are already shown
Sometimes you would like to perform some additional query after the item is already shown.
In this case, just perform the query in the background and bind the data to the item.
One caveat of this task is that if the view of the item is recycled (as is the case in RecyclerView by default), then when your background fetch is done the target View might show totally different item. Binding the returned data to this View would be a mistake.
One way of handling this is to cancel the fetch. However, this is cumbersome and error prone in practice.
The easier way is just set transientState flag when the query is initiated, and clear it when the query is completed.
I am extending the ArrayAdapter and I provided my own OnItemClickListener where I update certain states of my data which reflects on multiple items in the list so to update the current states of those items in the list, I am calling the notifyDataSetChanged. Its working properly as how I want it to my worry is if there are any issues or negative effects on doing so. Like when the user taps many items which results in consecutive calls to notifyDataSetChanged.
If the user interaction requires views to be updated then you do need to be calling notifyDataSetChanged each time. If you're really worried you can look at debounce algorithms but that's kind of overkill, but as long as you don't have a huge number of elements and don't add a bunch of data change listeners it shouldn't be a problem.
don't worried about calling notifyDataSetChanged it only notify adapter about data change
This is going to be a very straight-forward question.
When you perform a
adapter.notifyDataSetChanged(), ListView refreshes all it's content, by reloading every single row.
I want to avoid this relayout, so I've read this answer(Android - what is the meaning of StableIDs?):
Stable IDs allow the ListView to optimize for the case when items remain the same between
notifyDataSetChanged calls. The IDs it refers to are the ones returned
from getItemId.
Without it, the ListView has to recreate all Views since it can't know
if the item IDs are the same between data changes (e.g. if the ID is
just the index in the data, it has to recreate everything). With it,
it can refrain from recreating Views that kept their item IDs.
So I overrode that method and returned true, then I managed to get a unique ID for every row. However, when I trigger adapter.notifyDataSetChanged the listview recalculates everything.
The thing, is that I want to avoid this all recalculation, because the existing rows are not going to change, just several news are going to be added in the bottom (think about a pagination).
Is there any way to avoid full relayout of the ListView?
Thank you.
Is there a way to call BaseAdapter.notifyDataSetChanged() on a single element in the adapter.
What I am trying to do is update the data and reflect those changes in the containing ListView. The problem is that sometimes the change is so small that it seems ridiculous that I have to refresh the whole view rather than the single item in the view that has been updated.
I am not aware of such method. If it's really important, you can always find individual item view to update. But I don't think that it worth it as Android is pretty efficient in updating list views. So it will not do much extra work (definitelly not going beyond items currently visible on the screen).