RecyclerView update multiple locations - android

The notifyItemChanged method can only update one location at a timeļ¼Œand The notifyItemRangeChanged method can only update consecutive positions,How can I update multiple separate locations at once?

There is a really useful recyclerView item update blog here. Check it and use appropriate solution for your scenario. https://medium.com/#suragch/updating-data-in-an-android-recyclerview-842e56adbfd8

You can use notifyItemRangeChange when updating the data from and to in the position adapter
This example shows how to use it:
List<String> data;
data = new ArrayList<>();
//Add Anything list data
void AddMultipleItems() {
ArrayList<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("orange");
fruits.add("banana");
int fruitsIndex = 2;
data.addAll(insertIndex, fruits );
adapter.notifyItemRangeChanged(insertIndex, fruits.size());
}
Use this method if you have multiple data changed but not all , those changed data also are in cluster so that you can say from 25th to 30th index data are changed .
If all data are changed call :
notifyDataSetChanged();
Notes:
If you use notifyDataSetChanged(), then no animation will be performed. This can also be an expensive operation, so it is not recommended to use notifyDataSetChanged() if you are only updating a single item or a range of items.
Check out this link if you are making large or complex changes to a list.

In my case I had to update only two items, i.e., new and old item.
I put the old items position in a variable and update both new and old positions as below,
class FontAdapter(
private var curSelectedFontSource: Int
) : ListAdapter<FontItem, FontAdapter.MyViewHolder>(Companion) {
private var oldItemPos = -1
// other implementations
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val isSelected = getItem(position).fontSource ==
curSelectedFontSource
if (isSelected) {
oldItemPos = position
}
holder.binding.root.setOnClickListener {
notifyItemChanged(position)
notifyItemChanged(oldItemPos)
}
}
}

Related

How to stay at the same position in the RecyclerView even after fetching the updated data from the Firestore

I have a list of items in a RecyclerView that fetched from the Firestore database. Whenever I add, delete or edit an item I call respective function to make changes in the Firestore and then I call the getProductListFromFireStore() to update the RecyclerView. But the problem that I have is every time I make these changes, I go to the top of the list. I want to stay where I was even after adding/deleting or editing. How can I do that?
I didn't have the notifyDataSetChanged() in my code earlier, this was added to check if this will do the job, I do not know if I have done this right.
The following function is called when the product list is successfully fetched from the Firestore
fun successProductsListFromFireStore(productsList: ArrayList<Product>) {
hideProgressDialog()
if (productsList.size > 0) {
binding.rvMyProductItems.layoutManager = LinearLayoutManager(activity)
binding.rvMyProductItems.setHasFixedSize(true)
val adapterProducts =
MyProductsListAdapter(requireActivity(), productsList, this#ProductsFragment)
binding.rvMyProductItems.adapter = adapterProducts
adapterProducts.notifyDataSetChanged()
} else {
binding.rvMyProductItems.visibility = View.GONE
binding.tvNoProductsFound.visibility = View.VISIBLE
}
}
I think u need to use DiffUtill. It automatically update only changed items and if at least one item on screen was not changed then u will stay on the same position

DiffUtil.Callback() not working as the same as notifyDataSetChanged()

I have a message list that contains messages. And I used a regular RecyclerView.Adapter instead of ListAdapter.
ChatFragment
chatViewModel.msgListDisplayLiveData.observe(viewLifecycleOwner, {
chatAdapter.setChat(it)
})
Here is the function to update messageList in adapter.
ChatAdapter
fun setChat(newMessages: List<Message>) {
val diffUtil = ChatDiffUtil(messageList, newMessages)
val diffResults = DiffUtil.calculateDiff(diffUtil)
messageList = newMessages
diffResults.dispatchUpdatesTo(this)
--> Method of using notifyDataSetChanged()
// messageList = newMessages
// notifyDataSetChanged()
}
Previously I am using notifyDataSetChanged() and the list item can be updated instantly, but without any animation.
Thus, I decided to use DiffUtils() and I got the nice animation when my list's item got append/remove.
However, when I want to update one of the item, DiffUtils seems do not have any visible effect.
Using notifyDataSetChanged() - can instantly update the row.
Using DiffUtils() - cannot update instantly.
What had I done wrongly? Please advise me.

Diff Util implementation in Recycler scrolling it to the top on every refresh

I am using DiffUtil implementation to add new data to the RecyclerView on scroll.
This is the code that is used to update the items when required.
final DiffUtilsCallback<Movie> diffUtilsCallback = new DiffUtilsCallback<>(this.movies, movies);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffUtilsCallback);
this.movies.clear();
this.movies.addAll(movies);
diffResult.dispatchUpdatesTo(this);
Every time new items are added, the RecyclerView is scrolled to the top item. How do I make sure that the scroll doesn't happen when new items are added.
I have overwritten the objects equals method so that it compares objects with the ID.
#Override
public boolean equals(Object obj) {
if (!(obj instanceof Movie)) return false;
Movie movie = (Movie) obj;
return this.id == movie.id;
}
From what I can see in your code it can be that your diffutil callback used to see how to compare the movies is comparing object wise instead of by an internal id you may have per film.
If you are comparing the objects directly which is the default implementation, and since you are clearing your array, this comparison will fail for all the new objects that you are adding. You need to change how you compare the elements so diffutil does not interpret all of them as changed.
My setData of the adapter was like this:
fun setData(movies: List<Movies>, shouldClear: Boolean = false) {
val diffCallback = MovieDiffUtil(myDataSet, movies)
val diffMovies = DiffUtil.calculateDiff(diffCallback)
if (shouldClear) {
this.myDataSet.clear()
}
this.myDataSet.addAll(movies)
diffMovies.dispatchUpdatesTo(this)
}
I have pagination and movies are just the new set of data that does not include the old data. So while DiffUtils check for the difference, everything is new. So instead of sending only the new data, send all the data(old data + new upcoming data) to setData()
Let me know if I am not clear.

RecyclerView Adapter notifyDataSetChanged stops fancy animation

I am building a component based on RecyclerView, allowing user to reorder items by drag and drop.
Once I am on the DragListener side, I need the position it has in the adapter in order to perform correct move, but I only have access to the view.
So here is what I am doing in the adapter view binding :
#Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
Track track = mArray.get(position);
viewHolder.itemView.setTag(R.string.TAG_ITEM_POSITION, position);
}
Does it seem correct to you ?
Because if I move an item like this :
public void move(int from, int to){
Track track = mArray.remove(from);
mArray.add(to, track);
notifyItemMoved(from, to);
}
then position tag is not correct anymore, and if I notifyDataSetChanged(), I lose the fancy animation.
Any suggestion ?
There is a way to preserve fancy animations with just notifyDataSetChanged()
You need to make your own GridLayoutManager with overriden supportsPredictiveItemAnimations() method returning true;
You need to mAdapter.setHasStableIds(true)
The part I find tricky is you need to override you adapter's getItemId() method. It should return value that is truly unique and not a direct function of position. Something like mItems.get(position).hashCode()
Worked perfectly fine in my case - beautiful animations for adding, removing and moving items only using notifyDataSetChanged()
No, it is wrong. First of all, you cannot reference to the position passed to the onBindViewHolder after that method returns. RecyclerView will not rebind a view when its position changes (due to items moving etc).
Instead, you can use ViewHolder#getPosition() which will return you the updated position.
If you fix that, your move code should work & provide nice animations.
Calling notifyDataSetChanged will prevent predictive animations so avoid it as long as you can. See documentation for details.
Edit (from comment): to get position from the outside, get child view holder from recyclerview and then get position from the vh. See RecyclerView api for details
1) You'll use notifyItemInserted(position); or notifyItemRemoved(position); instead of notifyDataSetChanged() for animation.
2) You can just manually fix your problem - using
public void move(int from, int to){
Track track = mArray.remove(from);
mArray.add(to, track);
notifyItemMoved(from, to);
ViewHolder fromHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(from);
ViewHolder toHolder = (ViewHolder) mRecyclerView.findViewHolderForPosition(to);
Tag fromTag = fromHolder.itemView.getTag();
fromHolder.itemView.setTag(toHolder.itemView.getTag());
toHolder.itemView.setTag(fromTag);
}
You should move your method to OnCreateViewHolder, then notifyItemRemoved(index) works properly.
I'm able to maintain the touch animations by adding this to my list item's outer element
<View
android:foreground="?android:attr/selectableItemBackground"
...>
I fixed it with using 'notifyItemChanged(int position);' instead of 'notifyDataSetChanged();'
My adapter shows fancy animations perfectly and without any lags
Edit: I got position from onBindViewHolder's position.
as stated by others above, you can have animation while using notifyDataSetChanged on your adapter, although you need to specifically use stable ids. if your items IDs are strings, you can generate a long id for each string id you have and keep them in a map. for example:
class StringToLongIdMap {
private var stringToLongMap = HashMap<String, Long>()
private var longId: Long = 0
fun getLongId(stringId: String): Long {
if (!stringToLongMap.containsKey(stringId)) {
stringToLongMap[stringId] = longId++
}
return stringToLongMap[stringId] ?: -1
}
}
and then in your adapter:
private var stringToLongIdMap = StringToLongIdMap()
override fun getItemId(position: Int): Long {
val item = items[position]
return stringToLongIdMap.getLongId(item.id)
}
another useful thing to consider, if you are using kotlin data class as items in your adapter, and you don't have an id, you can use the hashCode of the data class itself as stable id (if you are sure that the item properties combination are unique in your data set):
override fun getItemId(position: Int): Long = items[position].hashCode().toLong()

RecyclerView change data set

I want to implement search functionality for my RecyclerView. On text changed i want to change the data that are displayed with this widget. Maybe this question has been asked before or is simple, but I don't know how the change the data that is to be shown...
My RecyclerView is defined as follows:
// 1. get a reference to recyclerView
mRecyclerView = (RecyclerView)findViewById(R.id.recyclerView);
// 2. set layoutManger
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 3. create an adapter
mAdapter = new ItemsAdapter(itemsData);
// 4. set adapter
mRecyclerView.setAdapter(mAdapter);
And the data that I am showing is something like:
ItemData itemsData[] = { new ItemData("Mary Richards"),
new ItemData("Tom Brown"),
new ItemData("Lucy London")
};
So when when I want to give the adapter another set of data, another array (with one item for example), what should I do?
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).
A better approach would be finding which items are removed and calling notifyItemRemoved(index). Don't forget to actually remove the item. This will let RecyclerView run predictive animations. Assuming you have an Adapter that internally uses an ArrayList, implementation would look like this:
// adapter code
final List<ItemData> mItems = new ArrayList(); //contains your items
public void filterOut(String filter) {
final int size = mItems.size();
for(int i = size - 1; i>= 0; i--) {
if (mItems.get(i).test(filter) == false) {
mItems.remove(i);
notifyItemRemoved(i);
}
}
}
It would perform even better if you can batch notifyItemRemoved calls and use notifyItemRangeRemoved instead. It would look sth like: (not tested)
public void filterOut(String filter) {
final int size = mItems.size();
int batchCount = 0; // continuous # of items that are being removed
for(int i = size - 1; i>= 0; i--) {
if (mItems.get(i).test(filter) == false) {
mItems.remove(i);
batchCount ++;
} else if (batchCount != 0) { // dispatch batch
notifyItemRangeRemoved(i + 1, batchCount);
batchCount = 0;
}
}
// notify for remaining
if (batchCount != 0) { // dispatch remaining
notifyItemRangeRemoved(0, batchCount);
}
}
You need to extend this code to add items that were previously filtered out but now should be visible (e.g. user deletes the filter query) but I think this one should give the basic idea.
Keep in mind that, each notify item call affects the ones after it (which is why I'm traversing the list from end to avoid it). Traversing from end also helps ArrayList's remove method performance (less items to shift).
For example, if you were traversing the list from the beginning and remove the first two items.
You should either call
notifyItemRangeRemoved(0, 2); // 2 items starting from index 0
or if you dispatch them one by one
notifyItemRemoved(0);
notifyItemRemoved(0);//because after the previous one is removed, this item is at position 0
This is my answer - thanks to Ivan Skoric from his site: http://blog.lovelyhq.com/creating-lists-with-recyclerview-in-android/
I created an extra method inside my adapter class:
public void updateList(List<Data> data) {
mData = data;
notifyDataSetChanged();
}
Then each time your data changes, you just call this method passing in your new data and your view should change to reflect it.
Just re-initialize your adapter:
mAdapter = new ItemsAdapter(newItemsData);
or if you only need to remove add a few specific items rather than a whole list:
mAdapter.notifyItemInserted(position);
or
mAdapter.notifyItemRemoved(position);
If you want to change the complete Adapter in the recycler view. you can just simply set by recycler.setAdapter(myAdapter);
It will automatically remove the old adapter from recycler view and replace it with your new adapter.
As ygit answered, swapAdapter is interesting when you have to change the whole content.
But, in my FlexibleAdapter, you can update the items with updateDataSet. You can even configure the adapter to call notifyDataSetChanged or having synchronization animations (enabled by default). That, because notifyDataSetChanged kills all the animations, but it's good to have for big lists.
Please have a look at the description, demoApp and Wiki pages: https://github.com/davideas/FlexibleAdapter

Categories

Resources