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?
Related
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.
The setup - RecyclerView / GridLayoutManager / custom SpanSizeLookup. Nothing unusual, it's just a grid with headers that span the entire width.
However what I've noticed is that performance can severely degrade the more and more items you have in the adapter. I've profiled this and it is 100% because of GetSpanSize in the custom lookup. It's a basic function, but it seems to be called for every item on every frame when scrolling. Sure enough, if I take out my lookup, performance is great regardless of how many items I have. As a use case this could be thousands of items. I start to see performance problems after maybe 1,000 items.
This sounds awfully inefficient from Android's side of things. I've searched high and low for someone else with this problem, but can't seem to find anything.
Any ideas?
Thanks,
EDIT: Added the code, however this still happens even if I just return 1 instead of doing the actual lookup. This issue seems to be that this is called for EVERY item in the adapter EVERY time the list moves when scrolling.
public class SpanSizeLookup : GridLayoutManager.SpanSizeLookup
{
//
private GridLayoutManager LayoutManager;
private MyItemAdapter ItemAdapter;
//
// SpanSizeLookup
public SpanSizeLookup( GridLayoutManager layoutManager, MyItemAdapter itemAdapter )
{
LayoutManager = layoutManager;
ItemAdapter = itemAdapter;
}
// GetSpanSize
public override int GetSpanSize( int position )
{
switch ( ItemAdapter.GetItemViewType( position ) )
{
case TYPE_HEADER:
return LayoutManager.SpanCount;
case TYPE_ITEM:
return 1;
}
return -1;
}
}
// GetItemViewType
public override int GetItemViewType( int position )
{
if ( ItemList[ position ].GUID.Equals( Guid.Empty ) )
return TYPE_HEADER;
return TYPE_ITEM;
}
The problem is you're not also overriding GetSpanIndex. The DefaultSpanSizeLookup is essentially this
public class DefaultSpanSizeLookup : GridLayoutManager.SpanSizeLookup
{
public int GetSpanSize(int position)
{
return 1;
}
public int GetSpanIndex(int position, int spanCount)
{
return position % spanCount;
}
}
If you override GetSpanIndex with the same value, you'll get the same performance as if you used DefaultSpanSizeLookup.
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);
}
I'm working on a android chatting application. When I called my api it returns me the chat list sorted by a user_id. But what I need to do is serialized by message_id as I want to show last message first.Here is my onBindViewHolder method in which i get the values.
public void onBindViewHolder(final MyAdapter_HomeViewHolder holder, final int position) {
holder.userNameTV.setText(data.get(position).getUserInfo().getFullName());
holder.msgBodyTV.setText(data.get(position).getBody());
holder.originator_iD.setText(data.get(position).getUserInfo().getId().toString());
//message ID. I need to serialize my recyclerView by this ID.biggest id should appear first.
holder.messageId.setText(data.get(position).getId().toString());
holder.owner_type_ET.setText("1");
holder.subject_ET.setText("Message");
}
In case of you need to see the full code, https://pastebin.com/Zxmq36Gn
try this before passing your list to the adapter (after API call and before adapter notifydatasetchanged):
Collections.sort(data, new Comparator<CustomData>() {
#Override
public int compare(CustomData lhs, CustomData rhs) {
// -1 - less than, 1 - greater than, 0 - equal, all inversed for descending
return lhs.getId() > rhs.getId() ? -1 : (lhs.customInt < rhs.customInt ) ? 1 : 0;
}
});
in Kotlin use like this after loading data in array:
myItems.sortWith(Comparator { lhs, rhs ->
// -1 - less than, 1 - greater than, 0 - equal, all inversed for descending
if (lhs.name > rhs.name) -1 else if (lhs.id < rhs.id) 1 else 0
})
After that apply:
myItemAdapter.notifyDataSetChanged()
A simpler solution for someone still stuck on the same
Collections.sort(data, new Comparator<CustomData>() {
#Override
public int compare(CustomData lhs, CustomData rhs) {
return Integer.compare( rhs.getId(),lhs.getId());
}
});
YourAdapter adapter = new YourAdapter(context, data);
//Setup Linear or Grid Layout manager
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
Before passing the data to RecyclerView adapter
data.sort(new Comparator<Datum>() {
#Override
public int compare(Datum o1, Datum o2) {
return o1.get(position).getMessageId().compareTo(o2.get(position).getMessageId());
}
});
then pass (notify) the sorted list to the adapter.
Add this lines of code before passing to RecyclerView Adapter
Collections.sort(yourLists, new Comparator<YourList>() {
#Override
public int compare(YourList lhs, YourList rhs) {
return lhs.getId().compareTo(rhs.getId());
}
});
Collections.sort(response.body(), new Comparator<All_posts>() {
#Override
public int compare(All_posts lhs, All_posts rhs) {
if(lhs.getId() > rhs.getId()) {
return -1;
} else {
return 1;
}
}
});
"response.body" is the arraylist I got from json, this is what I pass
to the recycler view adapter,
"All_posts" is the "Model" class, the one which contains only fields;
And getId is the value I want comparison to take place on it, it's from my model class,
I wrote this before I set my adapter to my recycler view.
and after I set my adpater to my recyclerView, I wrote recyclerView.getAdapter().notifyDataSetChanged();
List<Items_Detail_model>items_model;
Collections.sort(items_model, new Comparator<Items_Detail_model>() {
#Override
public int compare(Items_Detail_model o1, Items_Detail_model o2) {
return o1.getDate().compareTo(o2.getDate());
}
});
Try this before giving list to adapter
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Collections.sort(yourList, Comparator.comparing(YourModel::getFileSize));
}
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