I have a RecyclerView Adapter backed by a SortedList. If I make a change to an item, it both changes the item and repositions it in the list.
I've found that if I use notifyItemChanged on either the item's starting or ending position, it does not seem to have any effect even in conjunction with notifyItemMoved, either before or after.
If I use notifyItemMoved, it correctly triggers the movement animation, but the view does not change and still displays the outdated information.
If I use notifyDatasetChanged it updates the row and then moves it, but it does so sequentially which is slow, and it obviously notifies the entire list which is not exactly desirable.
Is there any way I can combine the moving and updating animations? And why doesn't notifyItemChanged do anything?
In RecyclerView.Adapter reference is said, that notifyItemMoved() is just structural change and therefore won't update data. On the other hand notifyItemChanged() is said to be data change.
When calling notifyItemChanged(), it will call RecyclerView#onBindViewHolder(), so it should update your view.
Working approach for me for updating and moving is:
notifyItemChanged(oldPos); notifyItemMoved(oldPos, newPos);
You can use:
SortedList.updateItemAt(int position, Objet newItem)
The newItem is the updated item, and position is the current position. This method replaces the current item for newItem and repositions it on the list (and the recyclerview link to it).
Here is the official documentation.
I hope this helps you.
Look at DiffUtil
https://developer.android.com/reference/android/support/v7/util/DiffUtil.html
When you update your dataset within your Adapter you can then use this tool to calculate the notifications needed to correctly represent your new data set.
Extend DiffUtil.Callback and implement the Abstract methods (I create a Constructor that looks like:
public MyDiffCallback(ArrayList<String> oldList, ArrayList<String> newList) {
this.oldList = oldList;
this.newList = newList;
}
I hold the oldList and newList in memory so that I can implement:
areItemsTheSame
areContentsTheSame
getNewListSize
getOldListSize
For example:
#Override
public int getOldListSize() {
return oldList.size();
}
#Override
public int getNewListSize() {
return newList.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).equals(newList(newItemPosition))
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return areItemsTheSame(oldItemPosition, newItemPosition);
}
areItemsTheSame: Tells the UTIL if the item has moved (checked position)
areContentsTheSame: Informs the UTIL if the contents of the item has changed.
Now in you updateDataSet method (or whatever you have called it!); do something like:
public updateDataSet(List newDataSet) {
// this.dataSet is the old data set / List
final MyDiffCallback callback = new MyDiffCallback(this.dataSet, newDataSet);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(callback);
this.dataSet = newDataSet;
diffResult.dispatchUpdatesTo(this); //This is the Adapter
}
Ref: https://medium.com/#iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00#.yhxirkkq6
Related
In my app I display a list of outfits in a 2 column GridLayout RecyclerView, and allow users to swipe an outfit to the side. Upon swiping, I update the viewIndex of the outfit in the database (an integer which it uses for sorting the result of the "get all outfits" query), which causes the query my LiveData (generated by Room) is watching to change and put that item at the end of the returned list. This in turn calls a setList method in my RecyclerViewAdapter which uses DiffUtil to update the list.
Everything works as expected in most cases. An item is swiped to the side, disappears, and if you scroll to the bottom of the RecyclerView you can find it again at the end.
However, when the position in the RecyclerView where this swiped item should appear (i.e. the bottom) is currently visible to the user, the item does not appear. If additional items are swiped while the end is still visible, they won't appear either.
Upon scrolling up and then back down, the items will now be in their proper places - it's fixed. I do not know why they are not rendered intially though - is this something to do with DiffUtil perhaps? It could also have to do with my solution to this bug, where I save and restore the state of the RecyclerView either side of the setList call to prevent it scrolling to the new location when the first item of the list is moved (see BrowseFragment below). I admit, I do not know exactly what that code does, I only know it fixed that problem. I tried commenting out those lines but it didn't affect the disappearing views.
How can I ensure the swiped items display immediately without requiring a scroll up? Below is a gif demonstrating the feature in use and then showing the problem (sorry for low quality, had to fit under 2MB).
Code in BrowseFragment.java where RecyclerView and Adapter are initialised:
RecyclerView outfitRecyclerView = binding.recyclerviewOutfits;
outfitsAdapter = new OutfitsAdapter(this, getViewLifecycleOwner(), this);
RecyclerView.LayoutManager layoutManager = new GridLayoutManager(requireActivity(), GRID_ROW_SIZE);
outfitRecyclerView.setLayoutManager(layoutManager);
outfitRecyclerView.setAdapter(outfitsAdapter);
//observe all outfits
outfitViewModel.getAllOutfits().observe(getViewLifecycleOwner(), (list) -> {
//save the state to prevent the bug where moving the first item of the list scrolls you to its new position
Parcelable recyclerViewState = outfitRecyclerView.getLayoutManager().onSaveInstanceState();
//set the list to the adapter
outfitsAdapter.setList(list);
outfitRecyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);
});
Room dao query used to generate the LiveData observed in outfitsViewModel.getAllOutfits()
#Query("SELECT * FROM outfits ORDER BY view_queue_index ASC")
LiveData<List<Outfit>> getAll();
setList method in OutfitsAdapter.java, where outfits is a private member variable containing the current list of outfits.
...
public void setList(List<Outfit> newList){
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new OutfitDiff(newList, outfits));
diffResult.dispatchUpdatesTo(this);
outfits = newList;
outfitsFull = new ArrayList<>(newList);
}
private class OutfitDiff extends DiffUtil.Callback {
List<Outfit> newList;
List<Outfit> oldList;
public OutfitDiff(List<Outfit> newList, List<Outfit> oldList) {
this.newList = newList;
this.oldList = oldList;
}
#Override
public int getOldListSize() {
if(oldList == null){
return 0;
}
return oldList.size();
}
#Override
public int getNewListSize() {
if(newList == null){
return 0;
}
return newList.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
}
}
I used notifyDataSetChanged in in my recycleriew. like this:
private void setList(List<Article> articles) {
mainList.addAll(articles);
notifyDataSetChanged();
}
But I want to use diffUtill in my recycleriew. I created my own diffUtill like this:
public class ArticleListDiffTool extends DiffUtil.Callback {
List<Article> oldList;
List<Article> newList;
private static final String TAG = "ArticleListDiffTool";
public ArticleListDiffTool(List<Article> oldList, List<Article> newList) {
this.oldList = oldList;
this.newList = newList;
Log.d(TAG, "ArticleListDiffTool: " + this.oldList.size() + "\n" + this.newList.size());
}
#Override
public int getOldListSize() {
return oldList.size();
}
#Override
public int getNewListSize() {
return newList.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).getId() .equals( newList.get(newItemPosition).getId());
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
}
#Nullable
#Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
//you can return particular field for changed item.
return super.getChangePayload(oldItemPosition, newItemPosition);
}
}
And I use it in my adapter :
private void setList(List articles) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new ArticleListDiffTool(this.mainList, articles),true);
mainList.addAll(articles);
diffResult.dispatchUpdatesTo(this);
}
I want to add new data to my old list. But when new data received, the recyclerView will be scrolled to the top of the list.
But I want to recyclerView be in its user state and new data add to the rest of the old list.
RecylcerView updates knows nothing about your views. When you call notifyDataSetChanged it tries to determine which views moved, or were replaced. I don't see you using setHasStableIds so when calling notifyDataSetChanged it will assume all of the content was replaced. It will jump to position 0 and be done with it. When you use setHasStableIds it will check the ids of the visible items and update the content in them. It will stop jumping around.
Now you also show that you are using DiffUtil. This is great! When you're not working with setHasStableIds this is the way to properly tell the recyclerView about what changed.
The problem you are facing is that you're using both. Either move to long ids and let the recyclerview do the diffing itself, or use DiffUtil and remove the call to notifyDataSetChanged. Either variant should work.
If you add new items at the end of the recyclerview, before you add the data, store the 1st visible item in recyclerview:
int position = ((LinearLayoutManager) recyclerView.GetLayoutManager()).findFirstVisibleItemPosition();
and after you make all the changes and call notifyDatasetChanged() scroll to position:
recyclerView.scrollToPosition(position);
I Found the problem after a few hours.
I every time that wants to update my recyclerView send received data to my adapter
And DiffUtil goes to compare my old list with received new parts and because of that (completely new items) DiffUtil decides to refresh whole recyclerView list.
Solution:
Now I get the current list from the recyclerView adapter and use addAll to insert new items that received from the server, then I pass this complete list to the adapter.
Now DiffUtil can compare diffrent between my old and new lists and recyclerView will be stay at it's current position.
I have a problem with my recyclerViev, specifically with the scrolling.
I have some list, which is updated in real time, some item is added, some removed, and everything is sorted by some parameter.
So the item which was initially first on the list, can have its parameter changed, which will be in different position after the sorting.
So my recyclerView is for example focusing on the initial item, and after change, when some item has "better" parameter is changing position with that initial item.
Problem is, i want to focus on the new item, with "better" parameter when I'm not scrolling, but i don't want to focusing on it when i scroll by touch(so my touch will not be interrupted by scrolling to current first item on the list).
So i don't want to force this code after every change in my recyclerView data:
recyclerView.scrollToPosition(0);
because as i said, i will be interrupted by this scroll when i am touching my recyclerView list and go down to see other items and in the same time there will be a change in my list.
Is there a way to accomplish this?
To be specific, i am using DiffCallback from the DiffUtil, to support animations when there is a change in my current recyclerView list - it compares the old list with another new list and apply all the wanted animations and notifications(item added, removed, changed position). So i never call
notifyDataSetChanged
or anything like that
Here is my DiffUtil callback:
public static class DevicesDiffCallback extends DiffUtil.Callback{
List<DeviceInfo> oldDevices;
List<DeviceInfo> newDevices;
public DevicesDiffCallback(List<NexoDeviceInfo> newDevices, List<NexoDeviceInfo> oldDevices) {
this.newDevices = newDevices;
this.oldDevices = oldDevices;
}
#Override
public int getOldListSize() {
return oldDevices != null ? oldDevices.size() : 0;
}
#Override
public int getNewListSize() {
return newDevices != null ? newDevices.size() : 0;
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldDevices.get(oldItemPosition).getNexoIdentifier().getSerialNumber().equals(newDevices.get(newItemPosition).getNexoIdentifier().getSerialNumber());
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldDevices.get(oldItemPosition).equals(newDevices.get(newItemPosition));
}
#Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return super.getChangePayload(oldItemPosition, newItemPosition);
}
}
And i set it like this in my adapter, when i get the list of new data to be populated and replace the old data:
public void setData(List<DeviceInfo> data) {
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DevicesDiffCallback(this.mData, data), false);
diffResult.dispatchUpdatesTo(this);
mData = data;
}
I'm not sure about this answer but, I think your code to call DiffUtil is not proper. Try using this :
public void addItems(List<Recipe> recipeList) {
List<Recipe> newRecipeList = new ArrayList<>();
newRecipeList.addAll(this.recipeList);
newRecipeList.addAll(recipeList);
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new RecipeDiffUtilCallback(this.recipeList, newRecipeList));
this.recipeList.addAll(recipeList);
diffResult.dispatchUpdatesTo(this);
}
Recently switched from notifyDataSetChanged to notifyItemInserted/Changed/Removed to preserve animations.
I'm stuck in this kind of situation. My adapter list initially is:
A
B
C
D
I then merge A, B and C to insert a new E item in last position:
A
E
I thought I could notify index 1 (B) as removed, index 2 (C) as removed, index 3 (D) as removed and then the new index 1 (E) as inserted, but an exception coming from I don't know where says:
Inconsistency detected. Invalid view holder adapter
positionViewHolder...
So I thought that maybe the problem was raised by notifying twice for index 1, so I changed it to notify index 1 (B) as changed to E, index 2 (C) as removed and index 3 (D) as removed, but the same exception was thrown, leaving me with no more options.
What should be the right approach in such a situation?
To not let question without answer in case someone else has the same problem
With Support library 24.2 Diffutils was released as a helpful class to calculate differences between two sets of items.
Sample code could look something like this (creating two lists and with press of button swapping the items in adapter). DiffUtil.DiffResult will handle all notify calls.
final List<RecyclerObject> list = new ArrayList<>();
list.add(new RecyclerObject("A"));
list.add(new RecyclerObject("B"));
list.add(new RecyclerObject("C"));
list.add(new RecyclerObject("D"));
final TestAdapter adapter = new TestAdapter(list);
recycler.setAdapter(adapter);
final List<RecyclerObject> newList = new ArrayList<>();
newList.add(new RecyclerObject("A"));
newList.add(new RecyclerObject("E"));
final View button = findViewById(R.id.mainButton);
button.setOnClickListener(v -> {
adapter.setList(newList);
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(list, newList));
diffResult.dispatchUpdatesTo(adapter);
});
Diffcallback is own created class, implementation could look something like this. (unnecessary code removed for better readability)
public class DiffCallback extends DiffUtil.Callback {
DiffCallback(final List<RecyclerObject> list, final List<RecyclerObject> newList) {
this.list = list;
this.newList = newList;
}
#Override
public int getOldListSize() {
return list.size();
}
#Override
public int getNewListSize() {
return newList.size();
}
#Override
public boolean areContentsTheSame(final int oldItemPosition, final int newItemPosition) {
return list.get(oldItemPosition).title.equals(newList.get(newItemPosition).title);
}
}
There is also one less fancy method : If we can guarantee that items backing the adapter are "stable" and same item will always have the same identificator, recycler.adapter allows for setHasStableIds option. With that, even using only notifyDatasetChanged (which normally kills animations) will correctly process them. Requires override of function getItemId()
I have a SortedList being displayed in a RecyclerView by my RecyclerView.Adapter.
I use 2 custom Comparator instances from withing the SortedListAdapterCallback.compare() method to either sort A-Z or Z-A.
static class A2Z implements Comparator<Item> {
#Override
public int compare(Item t0, Item t1) {
return t0.mText.compareTo(t1.mText);
}
}
static class Z2A extends A2Z {
#Override
public int compare(Item t0, Item t1) {
return -1 * super.compare(t0, t1);
}
}
Item simply contains a single String mText;
I use my comparators in the SortedListAdapterCallback.compare() method:
private Comparator<Item> a2z = new A2Z();
private Comparator<Item> z2a = new Z2A();
private Comparator<Item> comparator = z2a;
#Override
public int compare(Item t0, Item t1) {
return comparator.compare(t0, t1);
}
I change the comparators on a button press. The list on screen does not update.
After logging values in the various methods, I can tell that the list itself is not updating. Notifying the adapter of changes simply redraws the old list, without resorting it.
So how do I force the underlying SortedList to resort all the items?
Perhaps it is best to just create a new Adapter each time, as in this question:
RecyclerView change data set
SortedList does not have functionality to resort itself - each instance only has a single sort order.
Went with creating a new adapter for each resort, as per Yigit's answer to the above referenced question:
If you have stable ids in your adapter, you can get pretty good
results (animations) if you create a new array containing the filtered
items and call
recyclerView.swapAdapter(newAdapter, false);
Using swapAdapter hints RecyclerView that it can re-use view holders.
(vs in setAdapter, it has to recycle all views and re-create because
it does not know that the new adapter has the same ViewHolder set with
the old adapter).
Use a switch statement inside the compare method with a local control flag (an enum is a good idea).
After changing the switch flag, call sortedList.replaceAll.
#Override
public int compare(PmpRole pmpRoleA, PmpRole pmpRoleB) {
switch (mSorter){
case IDX:
return pmpRoleA.getIdx().compareTo(pmpRoleB.getIdx());
case TITLE:
return pmpRoleA.getTitleIdx().compareTo(pmpRoleB.getTitleIdx());
case ID_IDX:
return pmpRoleA.getIdIdx().compareTo(pmpRoleB.getIdIdx());
}
return -1;
}
public void setSorter(Sorter sorter){
mSorter = sorter;
mPmpRoleSortedList.replaceAll(mPmpRoles);
}
Maintains animation functionality etc.