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);
}
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 am using new ListAdapter, which automatically animates changes. I would like to disable animations or enable/disable it programmatically.
class UserAdapter extends ListAdapter<User, UserViewHolder> {
public UserAdapter() {
super(User.DIFF_CALLBACK);
}
#Override
public void onBindViewHolder(UserViewHolder holder, int position) {
holder.bindTo(getItem(position));
}
public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
new DiffUtil.ItemCallback<User>() {
#Override
public boolean areItemsTheSame(
#NonNull User oldUser, #NonNull User newUser) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldUser.getId() == newUser.getId();
}
#Override
public boolean areContentsTheSame(
#NonNull User oldUser, #NonNull User newUser) {
// NOTE: if you use equals, your object must properly override Object#equals()
// Incorrectly returning false here will result in too many animations.
return oldUser.equals(newUser);
}
}
}
Another solution is to simply remove the item animator altogether.
recyclerView.itemAnimator = null
You could try to disable or enable animations with setSupportsChangeAnimations on RecyclerView item animator:
SimpleItemAnimator itemAnimator = (SimpleItemAnimator) recyclerView.getItemAnimator();
itemAnimator.setSupportsChangeAnimations(false);
Solution which bypasses DiffUtil
Setting itemAnimator to null and calling submitList() still runs DiffUtil.ItemCallback in a background thread and doesn't submit the list on the same frame! If you want the same behaviour as calling notifyDataSetChanged(), you can do this:
adapter.submitList(null)
adapter.submitList(newItems)
This is useful if you know the contents are indeed completely different and don't care if you loose the scroll position. Or if you have multiple RecyclerViews that all have to update on the same frame (to reduce screen flashing).
Why does this work?
Looking at the source code of AsyncListDiffer.submitList():
// fast simple remove all
if (newList == null) {
//noinspection ConstantConditions
int countRemoved = mList.size();
mList = null;
mReadOnlyList = Collections.emptyList();
// notify last, after list is updated
mUpdateCallback.onRemoved(0, countRemoved);
onCurrentListChanged(previousList, commitCallback);
return;
}
// fast simple first insert
if (mList == null) {
mList = newList;
mReadOnlyList = Collections.unmodifiableList(newList);
// notify last, after list is updated
mUpdateCallback.onInserted(0, newList.size());
onCurrentListChanged(previousList, commitCallback);
return;
}
When you call submitList() the first time, all items are immediately removed. The second time they are immediately inserted, never calling DiffUtil or starting a background thread computation.
Putting it all together
if(animations) {
adapter.submitList(newItems)
} else {
recyclerView.itemAnimator = null
adapter.submitList(null)
adapter.submitList(newItems) {
recyclerView.post {
//Restore the default item animator
recyclerView.itemAnimator = DefaultItemAnimator()
}
}
}
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 find that all the information about DiffUtil on the internet is about how to use DiffUtil.
Is there any one who considers that using DiffUtil leads to large memory consuming?
If i have a list of 5000 objects(for example: post) in it, then i scroll RecyclerView and load 40 objects more. This means memory hold two lists(old list contains 5000 objects and new list contains 5040 objects) if using DiffUtil. So when loading data, the app consumes memory largely.
The way may solve this problem is that don't hold two lists. Showing as below:
DiffUtil.DiffResult recyclerViewDiff = DiffUtil.calculateDiff(new DiffUtil.Callback() {
#Override
public int getOldListSize() {
return oldelist.size();
}
#Override
public int getNewListSize() {
return oldlist.size() + increasedData.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
boolean isIncreaseDataPos = newItemPosition >= oldlist.size();
newItemPosition = !isIncreaseDataPos ? newItemPosition : newItemPosition - oldlist.size();
Object newItem = !isIncreaseDataPos ? oldlist.get(newItemPosition) : increaseData.get(newItemPosition);
return oldlist.get(oldItemPosition).equals(newItem);
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return ...;
}
});
But what if i change the item in list(not add more data)? Is there any way to use DiffUtil without holding two lists to update view except using notifyItemChanged(position) by myself?
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