I have a RecyclerView using a LinearLayoutManager, and a custom RecyclerView.Adapter. When a user long-clicks an item, it triggers an asynchronous network refresh of only that item. I know the item's position at the time of the long-click, and I can pass that position on to the network refreshing function. However by the time the refresh is complete and notifyItemChanged() is called, the user may have added a new item or removed one. So while the refreshed item may have originated from position 4, by the time the refresh is done it could be in 3 or 5 or somewhere else.
How can I ensure that I call notifyItemChanged() with the right position parameter?
Here are three possible solutions:
Call notifyDataSetChanged() instead and call it a day.
Keep a separate map of items by a unique ID in your adapter. Have the network refresh return item along with the unique ID. Access the item through the ID map and figure out its position. Obviously if there is no unique ID for your items, this isn't an option.
Keep track of the item(s) being refreshed. Register your own AdapterDataObserver and track all the inserts and updates, calculating the new position of the item each time and saving it until refresh returns.
While notifyDataSetChanged() will do the trick, if it is essential to know the position of the item, you can always go with implementation of hashCode and equals in the model class of the list item used in recyclerview adapter.
Implement hashcode and equals method to get the position for the required model object.
Example :
public class Employee {
protected long employeeId;
protected String firstName;
protected String lastName;
public boolean equals(Object o){
if(o == null) return false;
if(!(o instanceof) Employee) return false;
Employee other = (Employee) o;
if(this.employeeId != other.employeeId) return false;
if(! this.firstName.equals(other.firstName)) return false;
if(! this.lastName.equals(other.lastName)) return false;
return true;
}
public int hashCode(){
return (int) employeeId;
}
}
// To get the index of selected item which triggered async task :
int itemIndex = EmployeeList.indexOf(selectedEmployeeModel);
recyclerView.scrollToPosition(itemIndex);
Related
I have a RecyclerView to list a set of data. And on clicking each item , I have validation to check previous item is entered or not. If that item is not entered I want to enable an inline error (which is hidden in normal case) message in the previous row. I have done the scenario as shown below but error is showing only in the current row. Anyone suggest how I can enable/update previous row or a specific row.
public boolean _validateListItems(int itemIndex)
{
int previousItemIndex = itemIndex - 1;
for (int i = 0; i <= previousItemIndex; i++)
{
if ((listRecyclerItem.get(i).getEnable()==0))
{
return false;
}
}
return true;
}
holder.expand_button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(position>0){
if(_validateListItems(position))
{
mExpandedPosition = isExpanded ? -1:position;
notifyItemChanged(previousExpandedPosition);
notifyItemChanged(position);
notifyDataSetChanged();
}
else
{
holder.error.setVisibility(View.VISIBLE);
holder.error.setTextColor(ContextCompat.getColor(context, R.color.error_red));
}
}
}
});
First of all RecyclerView is something that recycle view. View is generated is based on Data Model.
So, lets store the user button/checkbox click/checked action) to the respective Model/Item. To run the validation, get the items from the Adapter and check your conditions in Activity/Fragment [Looping is an expensive operation, use Coroutine or RxJava]. Execute your validation and if Validation is true for an item, just update the Item from the list and finally update the Adapter. You can pass the Error message in the item and render it to the View. And finally, must use DiffUtil to update the items in adapter.
Declare an interface for your adapter that has callback to your viewModel and when each item changed you can return the call back ito viewmodel and store it in an object or array
for example :
interface Callback { void onDataChanged(int itemPosition); }
call that method in onBindViewHolder when your item text changed
and in view model add the returned item into a list
when you clicked on a button, you can check the items if your necessary item didn't exist you can return error
I have a simple recyclerview with items (tips) and a loading spinner at the bottom.
here's how the item count and item view type methods look:
#Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1) { // last position
return LOADING_FOOTER_VIEW_TYPE;
}
else {
return TIP_VIEW_TYPE;
}
}
#Override
public int getItemCount() {
return tips.size() + 1; // + 1 for the loading footer
}
basically, i just have a loading spinner under all my items.
I create the adapter once like so:
public TipsListAdapter(TipsActivity tipsActivity, ArrayList<Tip> tips) {
this.tipsActivity = tipsActivity;
this.tips = tips;
}
and then once i have fetched additional items, i call add like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeInserted(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
What's odd here is that when i do that, the scroll position goes to the very bottom. It almost seems like it followed the loading spinner. This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
This doesn't happen if i change notifyItemRangeInserted() to notifyItemRangeChanged() like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeChanged(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
Nor does it happen if i simply call notifyDataSetChanged() like so:
public void addTips(List<Tip> tips) {
this.tips.addAll(tips);
notifyDataSetChanged();
}
Here's the code for setting the adapter in my Activity:
public void setAdapter(#NonNull ArrayList<Tip> tips) {
if (!tips.isEmpty()) { // won't be empty if restoring state
hideProgressBar();
}
tipsList.setAdapter(new TipsListAdapter(this, tips));
}
public void addTips(List<Tip> tips) {
hideProgressBar();
getAdapter().addTips(tips);
restorePageIfNecessary();
}
private TipsListAdapter getAdapter() {
return (TipsListAdapter) tipsList.getAdapter();
}
Note:
I don't manually set scroll position anywhere.
I call setAdapter() in onResume()
addTips() is called after I fetch items from the server
Let me know if you need any additional parts of my code.
This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
RecyclerView has built-in behavior when calling the more-specific dataset change methods (like notifyItemRangeInserted() as opposed to notifyDataSetChanged()) that tries to keep the user looking at "the same thing" as before the operation.
When the data set changes, the first item the user can see is prioritized as the "anchor" to keep the user looking at approximately the same thing. If possible, the RecyclerView will try to keep this "anchor" view visible after the adapter update.
On the very first load, the first item (the only item) is the loading indicator. Therefore, when you load the new tips and update the adapter, this behavior will prioritize keeping the loading indicator on-screen. Since the loading indicator is kept at the end of the list, this will scroll the list to the bottom.
On subsequent loads, the first item is not the loading indicator, and it doesn't move. So the RecyclerView will not appear to scroll, since it doesn't have to do so to keep the "anchor" on-screen.
My recommendation is to check insertPos and see if it is zero. If it is, that means this is the first load, so you should update the adapter by calling notifyDataSetChanged() in order to avoid this anchoring behavior. Otherwise, call notifyItemRangeInserted() as you're currently doing.
Remove the setAdapter code from onResume ASAP as you are setting new TipsListAdapter(this, tips);
Every time a new reference of the adapter is created...make field mAdapter and then set it in onCreate . RecyclerView doesnt remember the scrolled position because everytime a new reference of adapter is being created.. onResume gets called infinitely when activity is in running state..
So either you setAdapter in onCreate using new operator to create reference for adapter or,
in onResume use mAdapter field variable reference..
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
I implement RecyclerView which have two ViewType. The list is dynamic and when user scroll down/up it add some items. In my RecyclerView just one of the item has different ViewType (consider this as expandable list which only one of item expand at a time).
I save position for expanded item But when new data added this position changed and I lost expanded item. Because data added in scroll down/up, updating expanded item according to size of new data is not good Idea.
One thing to mention is that I want to scroll to expanded item at first load. so I guess saving position would be best choice. (I guess but I'm not sure);
I want to know what's the efficient way to handle this issue?
http://tutorials.jenkov.com/java-collections/hashcode-equals.html
Implement hashcode and equals method, using this get the position for the expanded model object.
Example :
public class Employee {
protected long employeeId;
protected String firstName;
protected String lastName;
public boolean equals(Object o){
if(o == null) return false;
if(!(o instanceof) Employee) return false;
Employee other = (Employee) o;
if(this.employeeId != other.employeeId) return false;
if(! this.firstName.equals(other.firstName)) return false;
if(! this.lastName.equals(other.lastName)) return false;
return true;
}
public int hashCode(){
return (int) employeeId;
}
}
// To get the index of expanded item
int expandedItemIndex = EmployeeList.indexOf(expandedEmployeeModel);
recyclerView.scrollToPosition(expandedItemIndex);
Likely you use model for loading data to RecyclerView. And this model (for ex. ContactModel) contains different values for your ViewHolders.
What is point, that you use special references for that saved position. What you need to do, it's just put that position (of expanded item) to current model. And after all most works fine.
I want to filter/hide specific Items in the RecycleView if the item content matches with an preference set in SharedPreferences.
I guess I have to somehow prevent from these specific items from getting inflated in the adapter but I have no idea how.
Any ideas?
Cheers!
An adapter is the Model part of the Model-View-Controller design pattern for using ListView, GridView, and RecyclerView. So you have to think of it this way: The adapter, at any moment, has to reflect what you want to have displayed in the RecyclerView.
So here's an example: Let's say you have four items, and you want to filter the third item because it matches your preference. Your adapter's getCount() method must return 3. For getView(), position == 0 must return the first item view, position == 1 must return your second item view, and position == 2 must return your fourth item view.
It's up to your adapter code to figure out all calculations and offsets to make sure that it is always presenting a consistent state to the view. So for example, let's say you have a String array with the items, and an index dontshow pointing to the array item that shouldn't be displayed. You need to do something like this for getView():
int index = position; // position is input parameter
if (index >= dontshow) {
index++; // skip over the don't-show item
}
String item = items[index];
// now construct your view from this item
And
#Override
public int getCount() {
return items.length - 1;
}
Then when you make changes to your model, calling notifyDataSetChanged() tells the RecyclerView it has to call getCount() and getView() on your adapter all over again to redisplay the changed data.