When an item is added or removed using DiffUtil, inside android.support.v7.util.AdapterListUpdateCallback only the notifyItemRangeInserted(1, 1) or notifyItemRangeRemoved(1,1) respectively is invoked, notifyItemRangeChanged() is not invoked. I set the positions on each of the views using setTag(position) but are not getting updated for the existing items. Wouldn't prefer to update items manually.
list update:
private void updateItems(final List<Feed> newPosts) {
List<Feed> olderPosts = new ArrayList<>(currentPosts);
final CustomDiffCallback DIFF_CALLBACK = new CustomDiffCallback(olderPosts, newPosts);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(DIFF_CALLBACK, true);
}
Turns out that we can't use setTag() and getTag() for passing in position to the views. Since the main purpose of DiffUtil is to minimize the updations for views that haven't changed, the views that already have positions set on them cannot get through an updation pass hence would return stale positions. Though In my case, I had the custom listener in a separate class, the update part was a bit unusual. I had to get getLayoutPosition() of ViewHolder. Also can use getAdapterPosition() for the same.
Related
Is there an event called by changing the count of items in the recyclerview?
I want to call function every time recyclerview items are added.
Update:
It turns out I misunderstood the original question. What was really asked is how to get notified when the underlying data in an Adapter has changed.
For this you could use a RecyclerView.AdapterDataObserver in conjunction with RecyclerView.Adapter's registerAdapterDataObserver(AdapterDataObserver observer) method to register for changes in the underlying list data.
Original Answer:
Your RecyclerView is backed by a RecyclerView.Adapter which owns the actual list of items being displayed.
If you change the backing list, you should call one of the notifyXXX methods on your adapter to notify your RecyclerView what has changed (and optionally where in the list the change happened.)
At the very least, you could call notifyDataSetChanged on your adapter to tell your RecyclerView that something somewhere in the list has changed. This can be expensive since you're not being specific about what changed, so the RecyclerView has to query the adapter for more information and potentially redraw its entire client area of items.
Something like:
RecyclerView.Adapter adapter = recyclerView.getAdapter();
if (adapter != null) {
adapter.notifyDataSetChanged();
}
Behind the scenes, the RecyclerView will call the RecyclerView.Adapter#getItemCount() method on the Adapter to determine what the current item count is.
I have a recycler view I want to update one attribute of each item after the initial creation to provide the user with a nicer experience. I would like to do this asynchronously as it takes time to get the data.
How do you iterate over the items in a recycler view and subsequently update. I moved from listview to recycler because it has a the method NotifyItemChanged.
So Ideally I would like to do
void OnRefresh(IList<data> data)
{
Data = data;
NotifyDataSetChanged();
Task.Run(() => UpdateAllAttribute1Fields());
}
void UpdateAllAttribute1Fields()
{
foreach(var myItem in myRecyclerView.Items)
{
UpdateAttribute1(myItem));
}
}
But I do not understand how to access myItems. On windows (sorry), in a listview this would be listview.items I think.
I could save the views OnBindViewHolder but that will be a bit more work.
Thanks all.
You dont need to iterate over all items in recyclerview to update, Just set the RecyclerView to point to your custom data set. And asynchronously update the data set whenever you want. You can then notify the recyclerview that the data in the dataset has changed with the following methods of its adapter (which will automatically update the recyclerview content)
adapterObj.notifyItemChanged(pos) //for one object
adapterObj.notifyDataSetChanged() //for the entire dataset
adapterObj.notifyItemRangeChanged(start, end) //for a range
For instance, you have an adapter and in onBindViewHolder method you set OnClickListener to some views (and do some actions there depending on view position). You should assign final to position param of method onBindViewHolder so it could be accessible from onClick().
After changing dataset (remove or add item in list) you call onItemInserted or onItemRemoved and this really adds/removes a view in the recyclerview, BUT it does not refresh other viewitems so when you click on a neighbor viewitem it will open a screen or show data with invalid index. To avoid this I basically call notifyDatasetChanged to call onBind to all visible views and remove/add some views.
So how to refresh other views when you call notifyItemInserted/removed or how to work with these methots appropriately?
Assigning the position to a variable in onBindViewHolder will lead to an inconsistent state if items in the dataset are inserted or deleted without calling notifyDataSetChanged.
To use onItemInserted or onItemRemoved the data in the viewholder should remain consistent since it will not be redrawn and onClick would use this invalid position assigned before an item was added or removed.
For this and other use cases the RecyclerView.ViewHolder provides methods to access its position and id:
Use getAdapterPosition() or getItemId() to get valid positions and ids.
Also have a look on the other methods available in RecyclerView.ViewHolder.
So, the way I fix the problem I had is by changing the position into viewHolder.getAdapterPosition()
Cheers!
I advise you to add notifyItemRangeChanged after insert or remove list inside adapter. This work for my project.
Example in remove item :
public void removeItem (int pos) {
simpanList.remove(pos);
notifyItemRemoved(pos);
notifyItemRangeChanged(pos, simpanList.size());//add here, this can refresh position cmiiw
}
For future readers, this is what I do when inserting/removing in recyclerview
For example, my model class is CarsModel
In my adapter
ArrayList<CarsModel> carsModel;
In onBindViewHolder
CardModel model = carsModel.get(position);
When removing data in list using button in holder:
int position = holder.getAdapterPosition();
carsModel.remove(position);
notifyItemRemoved(position);
Then when inserting
carsModel.add(0, model);
notifyItemInserted(0);
or insert in last row
carsModel.add(carsModel.size() - 1 , model);
notifyItemInserted(carsModel.size()-1);
I'm having a bit of trouble preserving the scroll position of a list view when changing it's adapter's data.
What I'm currently doing is to create a custom ArrayAdapter (with an overridden getView method) in the onCreate of a ListFragment, and then assign it to its list:
mListAdapter = new CustomListAdapter(getActivity());
mListAdapter.setNotifyOnChange(false);
setListAdapter(mListAdapter);
Then, when I receive new data from a loader that fetches everything periodically, I do this in its onLoadFinished callback:
mListAdapter.clear();
mListAdapter.addAll(data.items);
mListAdapter.notifyDataSetChanged();
The problem is, calling clear() resets the listview's scroll position. Removing that call preserves the position, but it obviously leaves the old items in the list.
What is the proper way to do this?
As you pointed out yourself, the call to 'clear()' causes the position to be reset to the top.
Fiddling with scroll-position, etc. is a bit of a hack to get this working.
If your CustomListAdapter subclasses from ArrayAdapter, this could be the issue:
The call to clear(), calls 'notifyDataSetChanged()'. You can prevent this:
mListAdapter.setNotifyOnChange(false); // Prevents 'clear()' from clearing/resetting the listview
mListAdapter.clear();
mListAdapter.addAll(data.items);
// note that a call to notifyDataSetChanged() implicitly sets the setNotifyOnChange back to 'true'!
// That's why the call 'setNotifyOnChange(false) should be called first every time (see call before 'clear()').
mListAdapter.notifyDataSetChanged();
I haven't tried this myself, but try it :)
Check out: Maintain/Save/Restore scroll position when returning to a ListView
Use this to save the position in the ListView before you call .clear(), .addAll(), and . notifyDataSetChanged().
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
After updating the ListView adapter, the Listview's items will be changed and then set the new position:
mList.setSelectionFromTop(index, top);
Basically you can save you position and scroll back to it, save the ListView state or the entire application state.
Other helpful links:
Save Position:
How to save and restore ListView position in Android
Save State:
Android ListView y position
Regards,
Please let me know if this helps!
There is one more use-case I came across recently (Android 8.1) - caused by bug in Android code. If I use mouse-wheel to scroll list view - consecutive adapter.notifyDataSetChanged() resets scroll position to zero. Use this workaround until bug gets fixed in Android
listView.onTouchModeChanged(true); // workaround
adapter.notifyDataSetChanged();
More details is here: https://issuetracker.google.com/u/1/issues/130103876
In your Expandable/List Adapter, put this method
public void refresh(List<MyDataClass> dataList) {
mDataList.clear();
mDataList.addAll(events);
notifyDataSetChanged();
}
And from your activity, where you want to update the list, put this code
if (mDataListView.getAdapter() == null) {
MyDataAdapter myDataAdapter = new MyDataAdapter(mContext, dataList);
mDataListView.setAdapter(myDataAdapter);
} else {
((MyDataAdapter)mDataListView.getAdapter()).refresh(dataList);
}
In case of Expandable List View, you will use
mDataListView.getExpandableListAdapter() instead of
mDataListView.getAdapter()
I am having a situation where I want to update my Custom List View using BaseAdapter whenever my Database is updated. I have tried calling invalidate() on this Custom List but it didn't work, similarly I even tried having a timer to update my list after sometime, that didn't work either. Please let me know of possible solution.
Update:
This is how I am making my custom list view
li= (ListView)findViewById(R.id.id_lv_row);
ColorDrawable divcolor = new ColorDrawable(Color.DKGRAY);
registerForContextMenu(li);
li.setDivider(divcolor);
li.setDividerHeight(2);
li.setAdapter(new FriendsPositionAdapter(this));
BaseAdapter.notifyDataSetChanged() should do the trick as long as the data behind the adapter actually changed. That's all you need to do to refresh the list.
Invalidate is for repainting views only, you have to tell to the List adapter (BaseAdapter) that dataset has changed.
When the data changes, asign the new dataset to the adapter, and later call notifyDataSetChanged()...
in order to make functional notifyDataSetChanged() the adapter data must be changed. Remember that the original data that change is not reflected automatically to the adapter.
//here i retrieve the new list, named "beans"
lista = (BeanList) result.getDataObject();
Vector<Bean>beans = list.getBeanList();
((BeanListAdapter)listAdapter).syncData(beans);
((BeanListAdapter)listAdapter).notifyDataSetChanged();
//now the syncData method
public void syncData( List<PINPropiedad> newData ){
for(Object o : newData){
add(o);
}
}