UnsupportedOperationException: Alert dialogcrashing when positive button clicked - android

So i have this recycler view in which the last item is supposed to show a dialog on its click. the dialog is further comprising of 2 buttons, positive and negetive( the default ones).
On the press of Positive button, the dialog is supposed to trigger a callback which will add a new entry in the recycler view. On the press of the negetive button, the dialog simply dismisses.
Everything is working fine until when the user clicks positive button. the app crashes with the following log:
2019-08-13 18:27:35.668 29482-29482/z.y.x E/AndroidRuntime: FATAL EXCEPTION: main
Process: z.y.x, PID: 29482
java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at z.y.x.dashboard_files.dashboard_fragment.QuantityButtonsAdapter.addItemInCentre(QuantityButtonsAdapter.java:85)
at z.y.x.dashboard_files.dashboard_fragment.DashboardFragment$2$1.onPositiveButtonClick(DashboardFragment.java:144)
at z.y.x.dashboard_files.dashboard_fragment.DashboardFragment$QuantityDialog$3.onClick(DashboardFragment.java:253)
at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:172)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6692)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Here is the custom dialog class:
public static class QuantityDialog {
#Nullable
private QuantityButtonModel currentData;
private AlertDialog.Builder builder;
public interface OnPositiveClickListener {
void onPositiveButtonClick(QuantityButtonModel data);
}
public QuantityDialog(Context ctx) {
currentData = QUANTITY_GLASS;
View dialogView = createView(ctx);
builder = new AlertDialog.Builder(ctx)
.setView(dialogView)
.setCancelable(false)
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
});
}
// public void show() {
// show(null);
// }
#SuppressLint("InflateParams")
private View createView(Context ctx) {
View v = LayoutInflater.from(ctx)
.inflate(R.layout.dialog_new_quantitiy_btn, null);
//init ui
SeekBar seekQty = v.findViewById(R.id.seekbar_qty);
final TextView tvQty = v.findViewById(R.id.tv_qty_text_dialog);
final ImageView ivQty = v.findViewById(R.id.iv_qty_icon_dialog);
//init data and defaults
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
seekQty.setMin(0);
}
seekQty.setMax(QUANTITY_MAX);
if (currentData != null) {
seekQty.setProgress(currentData.getQty());
tvQty.setText(String.format(Locale.ROOT, "%d ml", currentData.getQty()));
ivQty.setImageResource(currentData.getQtyImage());
}
//init listener
seekQty.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean b) {
tvQty.setText(String.format(Locale.ROOT, "%d ml", progress));
int resID = getResForQty(progress);
ivQty.setImageResource(resID);
currentData = new QuantityButtonModel(resID, progress);
}
#Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
#Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
return v;
}
public void setOnPositiveClickListener(#Nullable final OnPositiveClickListener listener) {
builder.setPositiveButton("Done", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialogInterface, int i) {
if (listener != null) {
listener.onPositiveButtonClick(currentData);
}
dialogInterface.dismiss();
}
});
}
public void show() {
builder.show();
}
}
Here I am setting a listener to my recycler view's adapter. the listener is made in a way that if item is at any positon except last, it will show a toast, else it will show a dialog. The dialog is also not being reused(bad naming, i changed it later), but rather getting generated on every click and a click listener being attached at the same point, i.e on rv item's click.
adpButtons.setClickListener(new QuantityButtonClickListener() {
#Override
public void onItemClick(int qty) {
Toast.makeText(
fragView.getContext(),
"add:" + qty + " to shared preferences",
Toast.LENGTH_SHORT)
.show();
}
#Override
public void onAddNewItemClick() {
QuantityDialog reusableQuantityDialog;
reusableQuantityDialog = new QuantityDialog(fragView.getContext());
reusableQuantityDialog.setOnPositiveClickListener(
new OnPositiveClickListener() {
#Override
public void onPositiveButtonClick(QuantityButtonModel data) {
adpButtons.addItemInCentre(data);
}
});
reusableQuantityDialog.show();
}
});
I hope i tried to explain this clearly. I am guessing there is some kind of callback hell that is causing the problem. But i even tried reusing the dialogs which still caused the same error.
Please help.
Edit: Here is the adapter code:
public class QuantityButtonsAdapter extends RecyclerView.Adapter<QuantityButtonsAdapter.RvHolder> {
#NonNull
private List<QuantityButtonModel> buttonModelList;
private QuantityButtonClickListener clickListener;
QuantityButtonsAdapter() {
this(new ArrayList<QuantityButtonModel>(), null);
}
private QuantityButtonsAdapter(#NonNull List<QuantityButtonModel> buttonModelList,
QuantityButtonClickListener listener) {
this.buttonModelList = buttonModelList;
this.clickListener = listener;
}
#NonNull
#Override
public RvHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater
.from(parent.getContext())
.inflate(R.layout.layout_recycler_buttons, parent, false);
return new RvHolder(v);
}
#Override
public void onBindViewHolder(#NonNull RvHolder holder, int pos) {
QuantityButtonModel data = buttonModelList.get(pos);
//is last is a check based on which our code to add new data will get triggerred
boolean isLast = (pos==(buttonModelList.size()-1));
holder.bind(data.getQtyImage(), data.getQty(), clickListener, isLast);
}
#Override
public int getItemCount() {
return buttonModelList.size();
}
#NonNull
public List<QuantityButtonModel> getButtonModelList() {
return buttonModelList;
}
public void setButtonModelList(#NonNull List<QuantityButtonModel> buttonModelList) {
this.buttonModelList = buttonModelList;
notifyDataSetChanged();
}
public QuantityButtonClickListener getClickListener() {
return clickListener;
}
public void setClickListener(QuantityButtonClickListener clickListener) {
this.clickListener = clickListener;
notifyDataSetChanged();
}
public void addItemInCentre(QuantityButtonModel model) {
//int pos= buttonModelList.size()/2;
buttonModelList.add(model);
notifyDataSetChanged();
}
class RvHolder extends RecyclerView.ViewHolder {
ImageButton ibtQty;
TextView tvQty;
RvHolder(#NonNull View itemView) {
super(itemView);
ibtQty = itemView.findViewById(R.id.ibt_qty_btn);
tvQty = itemView.findViewById(R.id.tv_qty_text);
//ibtQty.setOnTouchListener(getMyItemTouchListener());
}
void bind(int qtyRes, final int qty, final QuantityButtonClickListener listener, final boolean isLast) {
ibtQty.setImageResource(qtyRes);
tvQty.setText(String.format(Locale.getDefault(), "%d ml", qty));
ibtQty.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
if (isLast) {
listener.onAddNewItemClick();
} else {
listener.onItemClick(qty);
}
showButtonPressAnimation(view);
}
});
}
}
interface QuantityButtonClickListener {
void onItemClick(int qty);
void onAddNewItemClick();
}
public static void showButtonPressAnimation(View view) {
final float shrinkTo = 0.90f;
final long duration = 100;
ScaleAnimation grow = new ScaleAnimation(
shrinkTo, 1,
shrinkTo, 1,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
grow.setDuration(duration / 2);
ScaleAnimation shrink = new ScaleAnimation(
1, shrinkTo,
1, shrinkTo,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
shrink.setDuration(duration / 2);
grow.setStartOffset(duration / 2);
AnimationSet set = new AnimationSet(true);
set.setInterpolator(new LinearInterpolator());
set.addAnimation(shrink);
set.addAnimation(grow);
view.startAnimation(set);
}
}

Didn't read through your Dialog source too much, but I don't expect any issue with it.
I guess the issue is adpButtons.addItemInCentre(data);, which you don't show in the question, but the addItemInCentre implementation is calling List.add on some list instance which is missing the add implementation.
Maybe it's some deserialized list, or some immutable one, can't recall from head any example, but I think there's some which can be created from Strings in XML, etc...
To have add available, you need list instance similar to ArrayList, etc...
So without showing this part of source, how the list (you try to add to) is defined and instantiated, it's difficult to tell precisely, what is the issue.
Edit: it's not clear from OP responses what precisely helped, but among the ways already mentioned above (deserialization, reflection), one can get the List<> instance with fixed size also by using the Arrays.asList(..) utility, which does NOT produce ArrayList, but specialized implementation of List<> which does bridge the original array with List<> interface (you can modify the original array through List<>.set(index, value) call), and thus it does refuse to do add/remove like operations, because underlying classic Type[] array is fixed size and the add/remove then would require new array, invalidating the original reference = makes no sense for asList functionality.
I.e. if you need implementation of java.util.List<E> interface which supports also add and remove like functionality of List, you must make sure the instance of your List<> is provided by class capable of these operations, like java.util.ArrayList<E> or java.util.Vector<E>, and avoid other implementations which do not support add/remove (like the one produced by Arrays.asList(..) call).
Also if you are writing Adapter, where you know you will need add/remove features of List<>, you may sometimes want to enforce particular List implementation, like ArrayList, on the API level, so when you later try to use that adapter with other implementation, it will fail at compile time. (then again having such specific API also prevents usage of other implementations which do support add/remove, and unfortunately the List<E> interface doesn't have direct interface derivative, which would ensure the add/remove are mandatory (they are optional in original List<E> interface), and could be used to narrow the API like that.

Related

recyclerview item mishap(undesirable behaviour) in android studio

I have a recycler view with a list of items and each of those items have checkboxes attached to them. When a checkbox is checked, it behaves properly and there is no problem with unwanted items getting checked. But when a checked item is deleted, then the unwanted item gets checked.
My Adapter Class :
public class TaskAdapter extends RecyclerView.Adapter<TaskAdapter.TaskHolder> {
private static List<Task> tasks = new ArrayList<>();
private static OnItemClickListener listener;
private static TaskAdapter adapter = new TaskAdapter();
#NonNull
#Override
public TaskHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.task_item, parent, false);
return new TaskHolder(itemView);
}
#Override
public void onBindViewHolder(#NonNull TaskHolder holder, int position) {
Task currentTask = tasks.get(position);
holder.a_tname.setText(currentTask.getTname());
holder.a_tdate.setText(currentTask.getTDate());
holder.a_ttime.setText(currentTask.getTTime());
holder.a_tprior.setText(currentTask.getTprior());
holder.bind(tasks.get(position));
holder.bind2(tasks.get(position));
}
#Override
public int getItemCount() {
return tasks.size();
}
public void setTasks(List<Task> tasks) {
this.tasks = tasks;
Collections.sort( tasks, Task.comparepriority);
notifyDataSetChanged();
}
public Task getTaskAt(int position){
return tasks.get(position);
}
class TaskHolder extends RecyclerView.ViewHolder {
private final TextView a_tname;
private final TextView a_tdate;
private final TextView a_ttime;
private final TextView a_tprior;
ImageView priorityIndicator;
CheckBox checkbox;
public TaskHolder(View itemView) {
super(itemView);
a_tname = itemView.findViewById(R.id.a_tname);
a_tdate=itemView.findViewById(R.id.a_tdate);
a_ttime = itemView.findViewById(R.id.a_ttime);
a_tprior = itemView.findViewById(R.id.a_tprior);
priorityIndicator = itemView.findViewById(R.id.priorityIndicator);
checkbox = itemView.findViewById(R.id.checkbox);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
int position = getAdapterPosition();
if(listener!=null&&position!=RecyclerView.NO_POSITION){
listener.onItemClick(tasks.get(position));
}
}
});
}
private void bind(Task task){
int drawableId;int red = R.color.red;int yellow = R.color.yellow;int green = R.color.green;
int color1 = ContextCompat.getColor(a_tprior.getContext(), red);
int color2 = ContextCompat.getColor(a_tprior.getContext(),yellow);
int color3 = ContextCompat.getColor(a_tprior.getContext(),green);
switch(task.t_prior){
case "1": drawableId = R.drawable.ic_baseline_priority_high_24;
a_tprior.setTextColor(color1);
break;
case "2": drawableId = R.drawable.ic_baseline_priority_middle_24;
a_tprior.setTextColor(color2);
break;
case "3" : drawableId = R.drawable.ic_baseline_low_priority_24;
a_tprior.setTextColor(color3);
break;
default: drawableId = R.drawable.ic_baseline_crop_square_24;
}
priorityIndicator.setImageDrawable(ContextCompat.getDrawable(priorityIndicator.getContext(),drawableId));
}
public void bind2(Task task){
final boolean[] checked = {true};
checkbox.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(checkbox.isChecked()) {
String pos = Integer.valueOf(getAdapterPosition()).toString();
PreferenceManager.getDefaultSharedPreferences(checkbox.getContext()).edit().
putBoolean("checkbox" + pos , checked[0]).apply();
Toast.makeText(checkbox.getContext(), "Way to go! Now swipe to delete", Toast.LENGTH_LONG).show();
}
else {
checked[0] =false;
String pos = Integer.valueOf(getAdapterPosition()).toString();
PreferenceManager.getDefaultSharedPreferences(checkbox.getContext()).edit().
putBoolean("checkbox" + pos, checked[0]).apply();
}
}
}); String pos = Integer.valueOf(getAdapterPosition()).toString();
boolean cb = PreferenceManager.getDefaultSharedPreferences(checkbox.getContext()).getBoolean
("checkbox" + pos, false);
checkbox.setChecked(cb);
}
}
public interface OnItemClickListener {
void onItemClick(Task ta);
}
public void setOnItemClickListener(OnItemClickListener listener){
this.listener = listener;
}
}
My code to delete in HomeFragment.java -
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
#Override
public boolean onMove(#NonNull RecyclerView recyclerView, #NonNull
RecyclerView.ViewHolder viewHolder, #NonNull RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(#NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
new AlertDialog.Builder(getActivity())
.setMessage("Do you want to delete this task?")
.setPositiveButton("Delete", ((dialog, which) ->
taskViewmodel.delete(adapter.getTaskAt(viewHolder.getAdapterPosition()))))
.setNegativeButton("Cancel", ((dialog, which) -> adapter.notifyItemChanged(position)))
.setOnCancelListener(dialog -> adapter.notifyItemChanged(position))
.create().show();
}
}).attachToRecyclerView(recyclerView);
Edit : I guess the problem is with the code associated with saving the state of the checkbox because the checkbox at a particular item position is checked so when the item is deleted, the below item takes its place and so it gets checked. Suppose item at 2nd position is checked, and I delete that item, then the item at the 3rd position takes its place and so that gets checked. I need to know how to resolve this.May i know what changes should i make to rectify this problem?
Thankyou
back at the day, there was no delete intuition, now since you're storing the state of whether an item checked or not in SharedPreferences using its position, if you have 3 items where position =2 is checked, and you delete this checked item at pos=2, then the third item would take its place behaving as new item at pos=2, that's why when you delete one, all checked states will get shifted.
I guess there's no option here but to use some identifier in your Task class, where one Task is uniquely identified using this number/string and you use that unique identifier to store the items state in your SharedPreferences as a key.
a quick cheap way to do it is to make Task class behave like how Room database identify its automatic unique int/long identifiers.
the way to do it is by
defining a static int/long counter field in your Task class that identifies how many ids you used so far so to not repeat any id that was taken before (even if it was deleted and not used now)
define a private int/long id field in the Task class, while in the Task class constructor when you're initializing a new Task you would increment the static counter field and use this new value as a value for your private id for your newly created Task.
N.B : you shouldn't increment the static value and assign a new id for every created task if the one you're creating an object for is an old one that you retrieved from SharedPreferences/Database that already have an old id, for that case you might have two constructors one that accepts an old id as paramter and one that increments the taskCounter and get a new id, you might also have two constructors that one calls the other incase you have some other paramteres you're passing to the task object at creation and you want to avoid duplicating code in both constructors.
that way when you're checking status using SharedPreferences for some task, you would use the private id value of Task instead of its position.
your Task class might look like this (in case of two constructors without any additional code in the constructor):
public class Task {
public static int tasksCounter =0;
public int taskId ;
...
//constructor for a new Task
public Task(){
this.taskId= ++tasksCounter ;
}
//constructor for an old Task
public Task(int oldId){
this.taskId= oldId ;
}
your Task class might look like this (in case of two constructors and you want to avoid code dublication):
public class Task {
public static int tasksCounter =0;
public int taskId ;
//you'd call that one if you're creating a completely new Task and it will call
the other constructor for you
public Task(){
Task(++tasksCounter)
}
//you'd call that one if you're creating a Task that you already have an id for
public Task(int id){
this.taskId= id ;
//some other code here
}
and in your adapter when you ask about whether its checked or not it would be like this :
PreferenceManager.getDefaultSharedPreferences(checkbox.getContext()).getBoolean
("checkbox" + task.taskId, false);
and when you the checkbox status get changed you'd change its value in preferences likewise using the task.taskId instead of position.
that of course would raise another problem that every time you start your application that static field will get reset to 0 again.
so you should store its value too in sharedpreferences maybe either
when you create a new Task, its value will be incremented so you should store the new value
or
just before the activity or fragment getting destroyed by overriding the method onDestroy() in either the activity or the fragment, you should store the last value it had
and when you start your activity or fragment you need to fetch it from sharedpreferences and assign it to Task.tasksCounter.
N.B : just incase you don't know, you get the value of static field by calling the class it self and you don't need to create a new object of this class to get its value, calling the next code is sufficent to get and edit its value :
Task.tasksCounter
and at the end,
since you have your complex data now (Task class), I would highly suggest to stop using SharedPreferences for storing everything and you start reading and switching to Room Database
Room Database provides you with necessary storing abilities of a Database including :
having an autoincrement identifier without having you worried about their values
storing and getting your data with just one line of code with a simple query instead of calling get with keys a 100 times.
with changing your Task class into an entity that have an autoincrement field you'll be good to go and ready to use Room to store your tasks.
I am not much familiar with lambda function so I am sharing how I would have did the same task.I tried this code for my own app and it worked perfectly fine.
Check the below code:
#Override
public void onSwiped(#NonNull RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAbsoluteAdapterPosition();//getAdapterPosition is depreciated, use getAbsoluteAdapterPosition
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setMessage("Do you want to delete this task?");
builder.setPositiveButton("Delete",new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
taskViewmodel.delete(adapter.getTaskAt(viewHolder.getAdapterPosition()))
adapter.notifyItemRemoved(position);
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
#Override
public void onCancel(DialogInterface dialog) {
adapter.notifyItemChanged(position);
}
}).show();
}
}).attachToRecyclerView(recyclerView);

Color picker wont change background color from RecyclerView Adapter?

I have an app where the user can use a color picker or pre-defined buttons to change the background of their quotes. I want to make it possible that the user can change the color of the predefined buttons with a longclick and then a color picker appears and the user can now choose to save any color to the button.
For some reason the color pickers listener methods dosn't work in my RecyclerView adapter. The color picker listener should give me the current seleceted color. I have tested it with a Log.d("TAG", ""+color);
and I dont get any values
The RecyclerView adapater's Viewholder for background colors:
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener, ColorPickerDialogFrag2.ColorPickerDialogListener {
public ImageButton colorButton;
public ViewHolder(View itemView) {
super(itemView);
this.colorButton = (ImageButton) itemView.findViewById(R.id.colorbutton);
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
#Override
public void onClick(View v) {
int position = getLayoutPosition();
MainActivity.relativeLayout.setBackgroundColor(Color.parseColor((mColorButtons.get(position).getColor())));
PreferenceManager.getDefaultSharedPreferences(App.getAppContext()).edit().putInt(App.BACKGROUND, Color.parseColor(mColorButtons.get(position).getColor())).apply();
//ColorPicker for customizing colors for buttons
#Override
public boolean onLongClick(View v) {
ColorPickerDialogFrag2 cp = ColorPickerDialogFrag2.newInstance(6, Color.YELLOW);
cp.setStyle(android.support.v4.app.DialogFragment.STYLE_NORMAL, R.style.AppTheme);
cp.show(fragmentManager,"d");
return false;
}
#Override
public void onPreviewColorChanged(int dialogId, int color) {
//color is the current color from the colorpicker
Log.d("TAG", ""+color);
MainActivity.mEditText.setBackgroundColor(color);
}
#Override
public void onColorSelected(int dialogId, int color) {
Log.d("TAG", ""+color);
MainActivity.mEditText.setBackgroundColor(color);
}
}
Here is an video of how it currently works in my app:
https://www.youtube.com/watch?v=lpYjKMLd9aU
The Color Picker libray I use is this: https://github.com/danielnilsson9/color-picker-view
The problem is that you implement ColorPickerDialogListener for your ViewHolder, but looking through the code of library here, you could see that it tries to cast activity as a ColorPickerDialogListener, otherwise it throws the exception. So try to implement this listener on your MainActivity and see the result.
UPDATE:
So in that case you need to change logic of your ColorPickerDialogFrag2 to something like this:
...
private ColorPickerDialogListener mListener;
public static ColorPickerDialogFrag2 newInstance(ColorPickerDialogListener listener,
int dialogId, int initialColor) {
ColorPickerDialogFrag2 frag = new ColorPickerDialogFrag2();
frag.setListener(listener);
Bundle args = new Bundle();
args.putInt("id", dialogId);
args.putInt("init_color", initialColor);
frag.setArguments(args);
return frag;
}
public void setListener(ColorPickerDialogListener mListener) {
this.mListener = mListener;
}
...
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if (mListener == null) {
// Check for listener in parent activity
try {
mListener = (ColorPickerDialogListener) activity;
} catch (ClassCastException e) {
e.printStackTrace();
throw new ClassCastException("Parent activity must implement "
+ "ColorPickerDialogListener to receive result.");
}
}
}
...
And then just pass ColorPickerDialogListener as a parameter to newInstance method:
#Override
public boolean onLongClick(View v) {
ColorPickerDialogFrag2 cp = ColorPickerDialogFrag2.newInstance(this, 6, Color.YELLOW);
cp.setStyle(android.support.v4.app.DialogFragment.STYLE_NORMAL, R.style.AppTheme);
cp.show(fragmentManager,"d");
return false;
}

RecyclerView getAdapterPosition() returns -1 on a callback so I can't show the new appearance for the item

Each item on my RecyclerView has a button that has three states: OPEN, LOADING, and CLOSED.
Initially all the buttons are in the OPEN state. When a button is clicked, the state is changed to LOADING and a network call is performed in the background. After the network call succeeds, the button state should be changed to CLOSED.
So in my adapter I used the following:
holder.button.setOnClickListener(v -> {
holder.state = LOADING;
notifyItemChanged(holder.getAdapterPosition()); /* 1 */
callNetwork(..., () -> {
/* this is the callback that runs on the main thread */
holder.state = CLOSED;
notifyItemChanged(holder.getAdapterPosition()); /* 2 */
});
});
The LOADING state is always visualized correctly at /* 1 */ because getAdapterPosition() gives me the correct position.
However, the CLOSED state of the button is never visualized, because getAdapterPosition at /* 2 */ always returns -1.
I might understand getAdapterPosition() wrongly in this case.
How do I refresh the appearance of an item on a callback?
From the docs:
Note that if you've called notifyDataSetChanged(), until the next
layout pass, the return value of this method will be NO_POSITION
NO_POSITION is a constant whose value is -1. This might explain why you are getting a return value of -1 here.
In any case, why don't you find the position of the model in the underlying dataset and then call notifyItemChanged(int position)? You could save the model as a field in the holder.
For example:
public class MyHolder extends RecyclerView.ViewHolder {
private Model mMyModel;
public MyHolder(Model myModel) {
mMyModel = myModel;
}
public Model getMyModel() {
return mMyModel;
}
}
holder.button.setOnClickListener(v -> {
holder.state = LOADING;
notifyItemChanged(holder.getAdapterPosition());
callNetwork(..., () -> {
/* this is the callback that runs on the main thread */
holder.state = CLOSED;
int position = myList.indexOf(holder.getMyModel());
notifyItemChanged(position);
});
});
Alternatively you can just ignore if the position is -1, like this:
holder.button.setOnClickListener(v -> {
holder.state = LOADING;
int preNetworkCallPosition = holder.getAdapterPosition();
if (preNetworkCallPosition != RecyclerView.NO_POSITION) {
notifyItemChanged(preNetworkCallPosition);
}
callNetwork(..., () -> {
/* this is the callback that runs on the main thread */
holder.state = CLOSED;
int postNetworkCallPosition = holder.getAdapterPosition();
if (postNetworkCallPosition != RecyclerView.NO_POSITION) {
notifyItemChanged(postNetworkCallPosition);
}
});
});
getAdapterPosition(); It will always return -1 when recyclerview makes layout calculations. You are calling this methods inside ViewHolder.. It means RecyclerView is doing calculations.
If you need position inside click actions of view, call it in the public void onClick(final View v) method for example:
"#Override
public void onBindViewHolder(#NonNull final ViewHolder holder, final int position) {
final Students user = mUsers.get(position);
holder.Name.setText(user.getFullname());
holder.Index.setText(user.getIndex_number());
if (user.getThumbnail().equals("default")) {
holder.profile_image.setImageResource(R.drawable.profile_pic);
} else {
Picasso.get().load(user.getThumbnail())
.placeholder(R.drawable.profile_pic)
.into(holder.profile_image);
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
**list_user_id = mUsers.get(position).getId();**
Intent Sub = new Intent(mContext, UserProfileActivity.class);
Sub.putExtra("user_id1", list_user_id);
mContext.startActivity(Sub);
BUT NOT
getAdapterPosition(); It will always return -1 when recyclerview makes layout calculations. You are calling this methods inside ViewHolder.. It means RecyclerView is doing calculations.
If you need position inside click actions of view, call it in the public void onClick(final View v) method for example:
#Override
public void onBindViewHolder(#NonNull final ViewHolder holder, final int position) {
final Students user = mUsers.get(position);
holder.Name.setText(user.getFullname());
holder.Index.setText(user.getIndex_number());
**list_user_id = mUsers.get(position).getId();**
if (user.getThumbnail().equals("default")) {
holder.profile_image.setImageResource(R.drawable.profile_pic);
} else {
Picasso.get().load(user.getThumbnail())
.placeholder(R.drawable.profile_pic)
.into(holder.profile_image);
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(final View v) {
Intent Sub = new Intent(mContext, UserProfileActivity.class);
Sub.putExtra("user_id1", list_user_id);
mContext.startActivity(Sub);

How to provide custom animation during sorting (notifyDataSetChanged) on RecyclerView

Currently, by using the default animator android.support.v7.widget.DefaultItemAnimator, here's the outcome I'm having during sorting
DefaultItemAnimator animation video : https://youtu.be/EccI7RUcdbg
public void sortAndNotifyDataSetChanged() {
int i0 = 0;
int i1 = models.size() - 1;
while (i0 < i1) {
DemoModel o0 = models.get(i0);
DemoModel o1 = models.get(i1);
models.set(i0, o1);
models.set(i1, o0);
i0++;
i1--;
//break;
}
// adapter is created via adapter = new RecyclerViewDemoAdapter(models, mRecyclerView, this);
adapter.notifyDataSetChanged();
}
However, instead of the default animation during sorting (notifyDataSetChanged), I prefer to provide custom animation as follow. Old item will slide out via right side, and new item will slide up.
Expected animation video : https://youtu.be/9aQTyM7K4B0
How I achieve such animation without RecylerView
Few years ago, I achieve this effect by using LinearLayout + View, as that time, we don't have RecyclerView yet.
This is how the animation is being setup
PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f);
PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", 0f, (float) width);
ObjectAnimator animOut = ObjectAnimator.ofPropertyValuesHolder(this, alpha, translationX);
animOut.setDuration(duration);
animOut.setInterpolator(accelerateInterpolator);
animOut.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator anim) {
final View view = (View) ((ObjectAnimator) anim).getTarget();
Message message = (Message)view.getTag(R.id.TAG_MESSAGE_ID);
if (message == null) {
return;
}
view.setAlpha(0f);
view.setTranslationX(0);
NewsListFragment.this.refreshUI(view, message);
final Animation animation = AnimationUtils.loadAnimation(NewsListFragment.this.getActivity(),
R.anim.slide_up);
animation.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
view.setVisibility(View.VISIBLE);
view.setTag(R.id.TAG_MESSAGE_ID, null);
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
view.startAnimation(animation);
}
});
layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animOut);
this.nowLinearLayout.setLayoutTransition(layoutTransition);
and, this is how the animation is being triggered.
// messageView is view being added earlier in nowLinearLayout
for (int i = 0, ei = messageViews.size(); i < ei; i++) {
View messageView = messageViews.get(i);
messageView.setTag(R.id.TAG_MESSAGE_ID, messages.get(i));
messageView.setVisibility(View.INVISIBLE);
}
I was wondering, how I can achieve the same effect in RecylerView?
Here is one more direction you can look at, if you don't want your scroll to reset on each sort (GITHUB demo project):
Use some kind of RecyclerView.ItemAnimator, but instead of rewriting animateAdd() and animateRemove() functions, you can implement animateChange() and animateChangeImpl(). After sort you can call adapter.notifyItemRangeChanged(0, mItems.size()); to triger animation.
So code to trigger animation will look pretty simple:
for (int i = 0, j = mItems.size() - 1; i < j; i++, j--)
Collections.swap(mItems, i, j);
adapter.notifyItemRangeChanged(0, mItems.size());
For animation code you can use android.support.v7.widget.DefaultItemAnimator, but this class has private animateChangeImpl() so you will have to copy-pasted code and changed this method or use reflection. Or you can create your own ItemAnimator class like #Andreas Wenger did in his example of SlidingAnimator. The point here is to implement animateChangeImpl Simmilar to your code there are 2 animations:
1) Slide old view to the right
private void animateChangeImpl(final ChangeInfo changeInfo) {
final RecyclerView.ViewHolder oldHolder = changeInfo.oldHolder;
final View view = oldHolder == null ? null : oldHolder.itemView;
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view == null) return;
mChangeAnimations.add(oldHolder);
final ViewPropertyAnimatorCompat animOut = ViewCompat.animate(view)
.setDuration(getChangeDuration())
.setInterpolator(interpolator)
.translationX(view.getRootView().getWidth())
.alpha(0);
animOut.setListener(new VpaListenerAdapter() {
#Override
public void onAnimationStart(View view) {
dispatchChangeStarting(oldHolder, true);
}
#Override
public void onAnimationEnd(View view) {
animOut.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
dispatchChangeFinished(oldHolder, true);
mChangeAnimations.remove(oldHolder);
dispatchFinishedWhenDone();
// starting 2-nd (Slide Up) animation
if (newView != null)
animateChangeInImpl(newHolder, newView);
}
}).start();
}
2) Slide up new view
private void animateChangeInImpl(final RecyclerView.ViewHolder newHolder,
final View newView) {
// setting starting pre-animation params for view
ViewCompat.setTranslationY(newView, newView.getHeight());
ViewCompat.setAlpha(newView, 0);
mChangeAnimations.add(newHolder);
final ViewPropertyAnimatorCompat animIn = ViewCompat.animate(newView)
.setDuration(getChangeDuration())
.translationY(0)
.alpha(1);
animIn.setListener(new VpaListenerAdapter() {
#Override
public void onAnimationStart(View view) {
dispatchChangeStarting(newHolder, false);
}
#Override
public void onAnimationEnd(View view) {
animIn.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationY(newView, 0);
dispatchChangeFinished(newHolder, false);
mChangeAnimations.remove(newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
Here is demo image with working scroll and kinda similar animation
https://i.gyazo.com/04f4b767ea61569c00d3b4a4a86795ce.gif
https://i.gyazo.com/57a52b8477a361c383d44664392db0be.gif
Edit:
To speed up RecyclerView preformance, instead of adapter.notifyItemRangeChanged(0, mItems.size()); you probably would want to use something like:
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisible = layoutManager.findFirstVisibleItemPosition();
int lastVisible = layoutManager.findLastVisibleItemPosition();
int itemsChanged = lastVisible - firstVisible + 1;
// + 1 because we start count items from 0
adapter.notifyItemRangeChanged(firstVisible, itemsChanged);
First of all:
This solution assumes that items that are still visible, after the dataset changed, also slide out to the right and later slide in from the bottom again (This is at least what I understood you are asking for)
Because of this requirement I couldn't find an easy and nice solution for this problem (At least during the first iteration). The only way I found was to trick the adapter - and fight the framework to do something that it was not intended for. This is why the first part (How it normally works) describes how to achieve nice animations with the RecyclerView the default way. The second part describes the solution how to enforce the slide out/slide in animation for all items after the dataset changed.
Later on I found a better solution that doesn't require to trick the adapter with random ids (jump to the bottom for the updated version).
How it normally works
To enable animations you need to tell the RecyclerView how the dataset changed (So that it knows what kind of animations should be run). This can be done in two ways:
1) Simple Version:
We need to set adapter.setHasStableIds(true); and providing the ids of your items via public long getItemId(int position) in your Adapter to the RecyclerView. The RecyclerView utilizes these ids to figure out which items were removed/added/moved during the call to adapter.notifyDataSetChanged();
2) Advanced Version: Instead of calling adapter.notifyDataSetChanged(); you can also explicitly state how the dataset changed. The Adapter provides several methods, like adapter.notifyItemChanged(int position),adapter.notifyItemInserted(int position),... to describe the changes in the dataset
The animations that are triggered to reflect the changes in the dataset are managed by the ItemAnimator. The RecyclerView is already equipped with a nice default DefaultItemAnimator. Furthermore it is possible to define custom animation behavior with a custom ItemAnimator.
Strategy to implement the slide out (right), slide in (bottom)
The slide to the right is the animation that should be played if items are removed from the dataset. The slide from bottom animation should be played for items that were added to the dataset. As mentioned at the beginning I assume that it is desired that all elements slide out to the right and slide in from the bottom. Even if they are visible before and after the dataset change. Normally RecyclerView would play to change/move animation for such items that stay visible. However, because we want to utilize the remove/add animation for all items we need to trick the adapter into thinking that there are only new elements after the change and all previously available items were removed. This can be achieved by providing a random id for each item in the adapter:
#Override
public long getItemId(int position) {
return Math.round(Math.random() * Long.MAX_VALUE);
}
Now we need to provide a custom ItemAnimator that manages the animations for the added/removed items. The structure of the presented SlidingAnimator is very similar to theandroid.support.v7.widget.DefaultItemAnimator that is provided with the RecyclerView. Also Notice this is a prove of concept and should be adjusted before used in any app:
public class SlidingAnimator extends SimpleItemAnimator {
List<RecyclerView.ViewHolder> pendingAdditions = new ArrayList<>();
List<RecyclerView.ViewHolder> pendingRemovals = new ArrayList<>();
#Override
public void runPendingAnimations() {
final List<RecyclerView.ViewHolder> additionsTmp = pendingAdditions;
List<RecyclerView.ViewHolder> removalsTmp = pendingRemovals;
pendingAdditions = new ArrayList<>();
pendingRemovals = new ArrayList<>();
for (RecyclerView.ViewHolder removal : removalsTmp) {
// run the pending remove animation
animateRemoveImpl(removal);
}
removalsTmp.clear();
if (!additionsTmp.isEmpty()) {
Runnable adder = new Runnable() {
public void run() {
for (RecyclerView.ViewHolder addition : additionsTmp) {
// run the pending add animation
animateAddImpl(addition);
}
additionsTmp.clear();
}
};
// play the add animation after the remove animation finished
ViewCompat.postOnAnimationDelayed(additionsTmp.get(0).itemView, adder, getRemoveDuration());
}
}
#Override
public boolean animateAdd(RecyclerView.ViewHolder holder) {
pendingAdditions.add(holder);
// translate the new items vertically so that they later slide in from the bottom
holder.itemView.setTranslationY(300);
// also make them invisible
holder.itemView.setAlpha(0);
// this requests the execution of runPendingAnimations()
return true;
}
#Override
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
pendingRemovals.add(holder);
// this requests the execution of runPendingAnimations()
return true;
}
private void animateAddImpl(final RecyclerView.ViewHolder holder) {
View view = holder.itemView;
final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
anim
// undo the translation we applied in animateAdd
.translationY(0)
// undo the alpha we applied in animateAdd
.alpha(1)
.setDuration(getAddDuration())
.setInterpolator(new DecelerateInterpolator())
.setListener(new ViewPropertyAnimatorListener() {
#Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
#Override
public void onAnimationEnd(View view) {
anim.setListener(null);
dispatchAddFinished(holder);
// cleanup
view.setTranslationY(0);
view.setAlpha(1);
}
#Override
public void onAnimationCancel(View view) {
}
}).start();
}
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
View view = holder.itemView;
final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
anim
// translate horizontally to provide slide out to right
.translationX(view.getWidth())
// fade out
.alpha(0)
.setDuration(getRemoveDuration())
.setInterpolator(new AccelerateInterpolator())
.setListener(new ViewPropertyAnimatorListener() {
#Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
#Override
public void onAnimationEnd(View view) {
anim.setListener(null);
dispatchRemoveFinished(holder);
// cleanup
view.setTranslationX(0);
view.setAlpha(1);
}
#Override
public void onAnimationCancel(View view) {
}
}).start();
}
#Override
public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
// don't handle animateMove because there should only be add/remove animations
dispatchMoveFinished(holder);
return false;
}
#Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
// don't handle animateChange because there should only be add/remove animations
if (newHolder != null) {
dispatchChangeFinished(newHolder, false);
}
dispatchChangeFinished(oldHolder, true);
return false;
}
#Override
public void endAnimation(RecyclerView.ViewHolder item) { }
#Override
public void endAnimations() { }
#Override
public boolean isRunning() { return false; }
}
This is the final result:
Update: While Reading the post again I figured out a better solution
This updated solution doesn't require to trick the adapter with random ids into thinking all items were removed and only new items were added. If we apply the 2) Advanced Version - how to notify the adapter about dataset changes, we can just tell the adapter that all previous items were removed and all the new items were added:
int oldSize = oldItems.size();
oldItems.clear();
// Notify the adapter all previous items were removed
notifyItemRangeRemoved(0, oldSize);
oldItems.addAll(items);
// Notify the adapter all the new items were added
notifyItemRangeInserted(0, items.size());
// don't call notifyDataSetChanged
//notifyDataSetChanged();
The previously presented SlidingAnimator is still necessary to animate the changes.

RecyclerView and java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder in Samsung devices

I have a recycler view that works perfectly on all devices except Samsung. On Samsung, I'm get
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder
when I'm going back to the fragment with the recycler view from another activity.
Adapter code:
public class FeedRecyclerAdapter extends RecyclerView.Adapter<FeedRecyclerAdapter.MovieViewHolder> {
public static final String getUserPhoto = APIConstants.BASE_URL + APIConstants.PICTURE_PATH_SMALL;
Movie[] mMovies = null;
Context mContext = null;
Activity mActivity = null;
LinearLayoutManager mManager = null;
private Bus uiBus = null;
int mCountOfLikes = 0;
//Constructor
public FeedRecyclerAdapter(Movie[] movies, Context context, Activity activity,
LinearLayoutManager manager) {
mContext = context;
mActivity = activity;
mMovies = movies;
mManager = manager;
uiBus = BusProvider.getUIBusInstance();
}
public void setMoviesAndNotify(Movie[] movies, boolean movieIgnored) {
mMovies = movies;
int firstItem = mManager.findFirstVisibleItemPosition();
View firstItemView = mManager.findViewByPosition(firstItem);
int topOffset = firstItemView.getTop();
notifyDataSetChanged();
if(movieIgnored) {
mManager.scrollToPositionWithOffset(firstItem - 1, topOffset);
} else {
mManager.scrollToPositionWithOffset(firstItem, topOffset);
}
}
// Create new views (called by layout manager)
#Override
public MovieViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.feed_one_recommended_movie_layout, parent, false);
return new MovieViewHolder(view);
}
// Replaced contend of each view (called by layout manager)
#Override
public void onBindViewHolder(MovieViewHolder holder, int position) {
setLikes(holder, position);
setAddToCollection(holder, position);
setTitle(holder, position);
setIgnoreMovieInfo(holder, position);
setMovieInfo(holder, position);
setPosterAndTrailer(holder, position);
setDescription(holder, position);
setTags(holder, position);
}
// returns item count (called by layout manager)
#Override
public int getItemCount() {
return mMovies != null ? mMovies.length : 0;
}
private void setLikes(final MovieViewHolder holder, final int position) {
List<Reason> likes = new ArrayList<>();
for(Reason reason : mMovies[position].reasons) {
if(reason.title.equals("Liked this movie")) {
likes.add(reason);
}
}
mCountOfLikes = likes.size();
holder.likeButton.setText(mContext.getString(R.string.like)
+ Html.fromHtml(getCountOfLikesString(mCountOfLikes)));
final MovieRepo repo = MovieRepo.getInstance();
final int pos = position;
final MovieViewHolder viewHolder = holder;
holder.likeButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(mMovies[pos].isLiked) {
repo.unlikeMovie(AuthStore.getInstance()
.getAuthToken(), mMovies[pos].id, new Callback<Movie>() {
#Override
public void success(Movie movie, Response response) {
Drawable img = mContext.getResources().getDrawable(R.drawable.ic_like);
viewHolder.likeButton
.setCompoundDrawablesWithIntrinsicBounds(img, null, null, null);
if (--mCountOfLikes <= 0) {
viewHolder.likeButton.setText(mContext.getString(R.string.like));
} else {
viewHolder.likeButton
.setText(Html.fromHtml(mContext.getString(R.string.like)
+ getCountOfLikesString(mCountOfLikes)));
}
mMovies[pos].isLiked = false;
}
#Override
public void failure(RetrofitError error) {
Toast.makeText(mContext.getApplicationContext(),
mContext.getString(R.string.cannot_like), Toast.LENGTH_LONG)
.show();
}
});
} else {
repo.likeMovie(AuthStore.getInstance()
.getAuthToken(), mMovies[pos].id, new Callback<Movie>() {
#Override
public void success(Movie movie, Response response) {
Drawable img = mContext.getResources().getDrawable(R.drawable.ic_liked_green);
viewHolder.likeButton
.setCompoundDrawablesWithIntrinsicBounds(img, null, null, null);
viewHolder.likeButton
.setText(Html.fromHtml(mContext.getString(R.string.like)
+ getCountOfLikesString(++mCountOfLikes)));
mMovies[pos].isLiked = true;
setComments(holder, position);
}
#Override
public void failure(RetrofitError error) {
Toast.makeText(mContext,
mContext.getString(R.string.cannot_like), Toast.LENGTH_LONG).show();
}
});
}
}
});
}
private void setComments(final MovieViewHolder holder, final int position) {
holder.likeAndSaveButtonLayout.setVisibility(View.GONE);
holder.commentsLayout.setVisibility(View.VISIBLE);
holder.sendCommentButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (holder.commentsInputEdit.getText().length() > 0) {
CommentRepo repo = CommentRepo.getInstance();
repo.sendUserComment(AuthStore.getInstance().getAuthToken(), mMovies[position].id,
holder.commentsInputEdit.getText().toString(), new Callback<Void>() {
#Override
public void success(Void aVoid, Response response) {
Toast.makeText(mContext, mContext.getString(R.string.thanks_for_your_comment),
Toast.LENGTH_SHORT).show();
hideCommentsLayout(holder);
}
#Override
public void failure(RetrofitError error) {
Toast.makeText(mContext, mContext.getString(R.string.cannot_add_comment),
Toast.LENGTH_LONG).show();
}
});
} else {
hideCommentsLayout(holder);
}
}
});
}
private void hideCommentsLayout(MovieViewHolder holder) {
holder.commentsLayout.setVisibility(View.GONE);
holder.likeAndSaveButtonLayout.setVisibility(View.VISIBLE);
}
private void setAddToCollection(final MovieViewHolder holder, int position) {
final int pos = position;
if(mMovies[position].isInWatchlist) {
holder.saveButton
.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_check_green, 0, 0, 0);
}
final CollectionRepo repo = CollectionRepo.getInstance();
holder.saveButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(!mMovies[pos].isInWatchlist) {
repo.addMovieToCollection(AuthStore.getInstance().getAuthToken(), 0, mMovies[pos].id, new Callback<MovieCollection[]>() {
#Override
public void success(MovieCollection[] movieCollections, Response response) {
holder.saveButton
.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_check_green, 0, 0, 0);
mMovies[pos].isInWatchlist = true;
}
#Override
public void failure(RetrofitError error) {
Toast.makeText(mContext, mContext.getString(R.string.movie_not_added_to_collection),
Toast.LENGTH_LONG).show();
}
});
} else {
repo.removeMovieFromCollection(AuthStore.getInstance().getAuthToken(), 0,
mMovies[pos].id, new Callback<MovieCollection[]>() {
#Override
public void success(MovieCollection[] movieCollections, Response response) {
holder.saveButton
.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_plus, 0, 0, 0);
mMovies[pos].isInWatchlist = false;
}
#Override
public void failure(RetrofitError error) {
Toast.makeText(mContext,
mContext.getString(R.string.cannot_delete_movie_from_watchlist),
Toast.LENGTH_LONG).show();
}
});
}
}
});
}
private String getCountOfLikesString(int countOfLikes) {
String countOfLikesStr;
if(countOfLikes == 0) {
countOfLikesStr = "";
} else if(countOfLikes > 999) {
countOfLikesStr = " " + (countOfLikes/1000) + "K";
} else if (countOfLikes > 999999){
countOfLikesStr = " " + (countOfLikes/1000000) + "M";
} else {
countOfLikesStr = " " + String.valueOf(countOfLikes);
}
return "<small>" + countOfLikesStr + "</small>";
}
private void setTitle(MovieViewHolder holder, final int position) {
holder.movieTitleTextView.setText(mMovies[position].title);
holder.movieTitleTextView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MovieDetailActivity.openView(mContext, mMovies[position].id, true, false);
}
});
}
private void setIgnoreMovieInfo(MovieViewHolder holder, final int position) {
holder.ignoreMovie.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MovieRepo repo = MovieRepo.getInstance();
repo.hideMovie(AuthStore.getInstance().getAuthToken(), mMovies[position].id,
new Callback<Void>() {
#Override
public void success(Void aVoid, Response response) {
Movie[] newMovies = new Movie[mMovies.length - 1];
for (int i = 0, j = 0; j < mMovies.length; i++, j++) {
if (i != position) {
newMovies[i] = mMovies[j];
} else {
if (++j < mMovies.length) {
newMovies[i] = mMovies[j];
}
}
}
uiBus.post(new MoviesChangedEvent(newMovies));
setMoviesAndNotify(newMovies, true);
Toast.makeText(mContext, mContext.getString(R.string.movie_ignored),
Toast.LENGTH_SHORT).show();
}
#Override
public void failure(RetrofitError error) {
Toast.makeText(mContext, mContext.getString(R.string.movie_ignored_failed),
Toast.LENGTH_LONG).show();
}
});
}
});
}
private void setMovieInfo(MovieViewHolder holder, int position) {
String imdp = "IMDB: ";
String sources = "", date;
if(mMovies[position].showtimes != null && mMovies[position].showtimes.length > 0) {
int countOfSources = mMovies[position].showtimes.length;
for(int i = 0; i < countOfSources; i++) {
sources += mMovies[position].showtimes[i].name + ", ";
}
sources = sources.trim();
if(sources.charAt(sources.length() - 1) == ',') {
if(sources.length() > 1) {
sources = sources.substring(0, sources.length() - 2);
} else {
sources = "";
}
}
} else {
sources = "";
}
imdp += mMovies[position].imdbRating + " | ";
if(sources.isEmpty()) {
date = mMovies[position].releaseYear;
} else {
date = mMovies[position].releaseYear + " | ";
}
holder.movieInfoTextView.setText(imdp + date + sources);
}
private void setPosterAndTrailer(final MovieViewHolder holder, final int position) {
if (mMovies[position] != null && mMovies[position].posterPath != null
&& !mMovies[position].posterPath.isEmpty()) {
Picasso.with(mContext)
.load(mMovies[position].posterPath)
.error(mContext.getResources().getDrawable(R.drawable.noposter))
.into(holder.posterImageView);
} else {
holder.posterImageView.setImageResource(R.drawable.noposter);
}
holder.posterImageView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MovieDetailActivity.openView(mActivity, mMovies[position].id, false, false);
}
});
if(mMovies[position] != null && mMovies[position].trailerLink != null
&& !mMovies[position].trailerLink.isEmpty()) {
holder.playTrailer.setVisibility(View.VISIBLE);
holder.playTrailer.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MovieDetailActivity.openView(mActivity, mMovies[position].id, false, true);
}
});
}
}
private void setDescription(MovieViewHolder holder, int position) {
String text = mMovies[position].overview;
if(text == null || text.isEmpty()) {
holder.descriptionText.setText(mContext.getString(R.string.no_description));
} else if(text.length() > 200) {
text = text.substring(0, 196) + "...";
holder.descriptionText.setText(text);
} else {
holder.descriptionText.setText(text);
}
final int pos = position;
holder.descriptionText.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
MovieDetailActivity.openView(mActivity, mMovies[pos].id, false, false);
}
});
}
private void setTags(MovieViewHolder holder, int position) {
List<String> tags = Arrays.asList(mMovies[position].tags);
if(tags.size() > 0) {
CastAndTagsFeedAdapter adapter = new CastAndTagsFeedAdapter(tags,
mContext, ((FragmentActivity) mActivity).getSupportFragmentManager());
holder.tags.setItemMargin(10);
holder.tags.setAdapter(adapter);
} else {
holder.tags.setVisibility(View.GONE);
}
}
// class view holder that provide us a link for each element of list
public static class MovieViewHolder extends RecyclerView.ViewHolder {
TextView movieTitleTextView, movieInfoTextView, descriptionText, reasonsCountText;
TextView reasonText1, reasonAuthor1, reasonText2, reasonAuthor2;
EditText commentsInputEdit;
Button likeButton, saveButton, playTrailer, sendCommentButton;
ImageButton ignoreMovie;
ImageView posterImageView, userPicture1, userPicture2;
TwoWayView tags;
RelativeLayout mainReasonsLayout, firstReasonLayout, secondReasonLayout, reasonsListLayout;
RelativeLayout commentsLayout;
LinearLayout likeAndSaveButtonLayout;
ProgressBar progressBar;
public MovieViewHolder(View view) {
super(view);
movieTitleTextView = (TextView)view.findViewById(R.id.movie_title_text);
movieInfoTextView = (TextView)view.findViewById(R.id.movie_info_text);
descriptionText = (TextView)view.findViewById(R.id.text_description);
reasonsCountText = (TextView)view.findViewById(R.id.reason_count);
reasonText1 = (TextView)view.findViewById(R.id.reason_text_1);
reasonAuthor1 = (TextView)view.findViewById(R.id.author_1);
reasonText2 = (TextView)view.findViewById(R.id.reason_text_2);
reasonAuthor2 = (TextView)view.findViewById(R.id.author_2);
commentsInputEdit = (EditText)view.findViewById(R.id.comment_input);
likeButton = (Button)view.findViewById(R.id.like_button);
saveButton = (Button)view.findViewById(R.id.save_button);
playTrailer = (Button)view.findViewById(R.id.play_trailer_button);
sendCommentButton = (Button)view.findViewById(R.id.send_button);
ignoreMovie = (ImageButton)view.findViewById(R.id.ignore_movie_imagebutton);
posterImageView = (ImageView)view.findViewById(R.id.poster_image);
userPicture1 = (ImageView)view.findViewById(R.id.user_picture_1);
userPicture2 = (ImageView)view.findViewById(R.id.user_picture_2);
tags = (TwoWayView)view.findViewById(R.id.list_view_feed_tags);
mainReasonsLayout = (RelativeLayout)view.findViewById(R.id.reasons_main_layout);
firstReasonLayout = (RelativeLayout)view.findViewById(R.id.first_reason);
secondReasonLayout = (RelativeLayout)view.findViewById(R.id.second_reason);
reasonsListLayout = (RelativeLayout)view.findViewById(R.id.reasons_list);
commentsLayout = (RelativeLayout)view.findViewById(R.id.comments_layout);
likeAndSaveButtonLayout = (LinearLayout)view
.findViewById(R.id.like_and_save_buttons_layout);
progressBar = (ProgressBar)view.findViewById(R.id.centered_progress_bar);
}
}
}
Exception:
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{42319ed8 position=1 id=-1, oldPos=0, pLpos:0 scrap tmpDetached no parent}
at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:4166)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4297)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4278)
at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1947)
at android.support.v7.widget.GridLayoutManager.layoutChunk(GridLayoutManager.java:434)
at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1322)
at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:556)
at android.support.v7.widget.GridLayoutManager.onLayoutChildren(GridLayoutManager.java:171)
at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:2627)
at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:2971)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.support.v4.widget.SwipeRefreshLayout.onLayout(SwipeRefreshLayout.java:562)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.support.v4.view.ViewPager.onLayout(ViewPager.java:1626)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.support.v4.view.ViewPager.onLayout(ViewPager.java:1626)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
07-30 12:48:22.688 9590-9590/com.Filmgrail.android.debug W/System.err? at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1677)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1531)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1440)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:453)
at android.widget.FrameLayout.onLayout(FrameLayout.java:388)
at android.view.View.layout(View.java:15746)
at android.view.ViewGroup.layout(ViewGroup.java:4867)
at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2356)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2069)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1254)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6630)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:803)
at android.view.Choreographer.doCallbacks(Choreographer.java:603)
at android.view.Choreographer.doFrame(Choreographer.java:573)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:789)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5479)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1283)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1099)
at dalvik.system.NativeStart.main(Native Method)
How can I fix this?
This problem is caused by RecyclerView Data modified in different thread. The best way is checking all data access. And a workaround is wrapping LinearLayoutManager.
Previous answer
There was actually a bug in RecyclerView and the support 23.1.1 still not fixed.
For a workaround, notice that backtrace stacks, if we can catch this Exception in one of some class it may skip this crash. For me, I create a LinearLayoutManagerWrapper and override the onLayoutChildren:
public class WrapContentLinearLayoutManager extends LinearLayoutManager {
//... constructor
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (IndexOutOfBoundsException e) {
Log.e("TAG", "meet a IOOBE in RecyclerView");
}
}
}
Then set it to RecyclerView:
RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new WrapContentLinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));
Actually catch this exception, and seems no any side-effect yet.
Also, if you use GridLayoutManager or StaggeredGridLayoutManager you must create a wrapper for it.
Notice: The RecyclerView may be in a wrong internal state.
This is an example for refreshing data with completely new content.
You can easily modify it to fit your needs.
I solved this in my case by calling:
notifyItemRangeRemoved(0, previousContentSize);
before:
notifyItemRangeInserted(0, newContentSize);
This is the correct solution and is also mentioned in this post by an AOSP project member.
I faced this issue once, and I solved this by wrapping the LayoutManager and disabling predictive animations.
Here an example:
public class LinearLayoutManagerWrapper extends LinearLayoutManager {
public LinearLayoutManagerWrapper(Context context) {
super(context);
}
public LinearLayoutManagerWrapper(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public LinearLayoutManagerWrapper(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
#Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
}
And set it to RecyclerView:
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManagerWrapper(context, LinearLayoutManager.VERTICAL, false);
New answer:
Use DiffUtil for all RecyclerView updates. This will help with both performance and the bug above.
See Here
Previous answer:
This worked for me. The key is to not use notifyDataSetChanged() and to do the right things in the correct order:
public void setItems(ArrayList<Article> newArticles) {
//get the current items
int currentSize = articles.size();
//remove the current items
articles.clear();
//add all the new items
articles.addAll(newArticles);
//tell the recycler view that all the old items are gone
notifyItemRangeRemoved(0, currentSize);
//tell the recycler view how many new items we added
notifyItemRangeInserted(0, newArticles.size());
}
Reasons caused this issue:
An internal issue in Recycler when item animations are enabled
Modification on Recycler data in another thread
Calling notify methods in a wrong way
SOLUTION:
-----------------SOLUTION 1---------------
Catching the exception (Not recommended especially for reason #3)
Create a custom LinearLayoutManager as the following and set it to the ReyclerView
public class CustomLinearLayoutManager extends LinearLayoutManager {
//Generate constructors
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Inconsistency detected");
}
}
}
Then set RecyclerVIew Layout Manager as follow:
recyclerView.setLayoutManager(new CustomLinearLayoutManager(activity));
-----------------SOLUTION 2---------------
Disable item animations (fixes the issue if it caused de to reason #1):
Again, create a custom Linear Layout Manager as follow:
public class CustomLinearLayoutManager extends LinearLayoutManager {
//Generate constructors
#Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
}
Then set RecyclerVIew Layout Manager as follow:
recyclerView.setLayoutManager(new CustomLinearLayoutManager(activity));
-----------------SOLUTION 3---------------
This solution fixes the issue if it caused by reason #3. You need to make sure that you are using the notify methods in the correct way. Alternatively, use DiffUtil to handle the change in a smart, easy, and smooth way.
Using DiffUtil in Android RecyclerView
-----------------SOLUTION 4---------------
For reason #2, you need to check all data access to recycler list and make sure that there is no modification on another thread.
I had a similar problem.
Problem in error code below:
int prevSize = messageListHistory.size();
// some insert
adapter.notifyItemRangeInserted(prevSize - 1, messageListHistory.size() -1);
Solution:
int prevSize = messageListHistory.size();
// some insert
adapter.notifyItemRangeInserted(prevSize, messageListHistory.size() -prevSize);
According to this issue, the problem has been resolved and was likely released some time near the beginning of 2015. A quote from that same thread:
It is specifically related to calling notifyDataSetChanged. [...]
Btw, I strongly advice not using notifyDataSetChanged because it kills animations and performance. Also for this case, using specific notify events will work around the issue.
If you are still having issues with a recent version of the support library, I would suggest reviewing your calls to notifyXXX (specifically, your use of notifyDataSetChanged) inside your adapter, to make sure you are adhering to the (somewhat delicate/obscure) RecyclerView.Adapter contract. Also be sure to issue those notifications on the main thread.
I had the same problem.
It was caused because I delayed notification for adapter about item insert.
But ViewHolder tried to redraw some data in it's view and it started the RecyclerView measuring and recounting children count - at that moment it crashed (items list and it's size was already updated, but the adapter was not notified yet).
another reason this problem happens is when you call these methods with wrong indexes (indexes which there has NOT happened insert or remove in them)
-notifyItemRangeRemoved
-notifyItemRemoved
-notifyItemRangeInserted
-notifyItemInserted
check indexe parameters to these methods and make sure they are precise and correct.
In my case, I was getting this problem because of getting data updates from server (I am using Firebase Firestore) and while the first set of data is being processed by DiffUtil in the background, another set of data update comes and causes a concurrency issue by starting another DiffUtil.
In short, if you are using DiffUtil on a Background thread which then comes back to the Main Thread to dispatch the results to the RecylerView, then you run the chance of getting this error when multiple data updates come in short time.
I solved this by following the advice in this wonderful explanation:
https://medium.com/#jonfhancock/get-threading-right-with-diffutil-423378e126d2
Just to explain the solution is to push the updates while the current one is running to a Deque. The deque can then run the pending updates once the current one finishes, hence handling all subsequent updates but avoiding inconsistency errors as well!
Hope this helps because this one made me scratch my head!
This happens when you specify the incorrect position for the notifyItemChanged ,
notifyItemRangeInserted etc.For me :
Before : (Erroneous)
public void addData(List<ChannelItem> list) {
int initialSize = list.size();
mChannelItemList.addAll(list);
notifyItemRangeChanged(initialSize - 1, mChannelItemList.size());
}
After : (Correct)
public void addData(List<ChannelItem> list) {
int initialSize = mChannelItemList.size();
mChannelItemList.addAll(list);
notifyItemRangeInserted(initialSize, mChannelItemList.size()-1); //Correct position
}
This bug is still not fixed in 23.1.1, but a common workaround would be to catch the exception.
This problem is caused by RecyclerView Data modified in different
thread
Can confirm threading as one problem and since I ran into the issue and RxJava is becoming increasingly popular: make sure that you are using .observeOn(AndroidSchedulers.mainThread()) whenever you're calling notify[whatever changed]
code example from adapter:
myAuxDataStructure.getChangeObservable().observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<AuxDataStructure>() {
[...]
#Override
public void onNext(AuxDataStructure o) {
[notify here]
}
});
In my case every time when I call notifyItemRemoved(0), it crashed. Turned out that I set setHasStableIds(true) and in getItemId I just returned the item position.
I ended out updating it to return the item's hashCode() or self-defined unique id, which solved the issue.
I ran into the same problem.
My app uses Navigation components with a fragment containing my recyclerView. My list displayed fine the first time the fragment was loaded ... but upon navigating away and coming back this error occurred.
When navigating away the fragment lifecycle went only through onDestroyView and upon returning it started at onCreateView. However, my adapter was initialized in the fragment's onCreate and did not reinitialize when returning.
The fix was to initialize the adapter in onCreateView.
Hope this may help someone.
Solved for me by updating the recycler view to the last version
implementation "androidx.recyclerview:recyclerview:1.2.1"
Problem occured for me only when:
I created the Adapter with an empty list.
Then I inserted items and called notifyItemRangeInserted.
Solution:
I solved this by creating the Adapter only after I have the first chunk of data and initialzing it with it right away. The next chunk could then be inserted and notifyItemRangeInserted called with no problem .
The error can be caused by your changes being inconsistent with what you are notifying. In my case:
myList.set(position, newItem);
notifyItemInserted(position);
What I of course had to do:
myList.add(position, newItem);
notifyItemInserted(position);
My problem was that even though i clear both the array list containing the data model for the recycler view, i did not notify the adapter of that change, so it had stale data from previous model. Which caused the confusion about the view holder position. To Fix this always notify the adapter that the data-set as changed before updating again.
In my case I was changing the data previously inside a thread with mRecyclerView.post(new Runnable...) and then again later changed data in the UI thread, which caused inconsistency.
I ran into the same problem when I have both removed and updated items in the list...
After days of investigating I think I finally found a solution.
What you need to do is first do all the notifyItemChanged of your list and only then do all the notifyItemRemoved in a descending order
I hope this will help people that are running into the same issue...
In my case the problem was that I used notifyDataSetChanged when amount of newly loaded data was less than initial data.
This approach helped me:
adapter.notifyItemRangeChanged(0, newAmountOfData + 1);
adapter.notifyItemRangeRemoved(newAmountOfData + 1, previousAmountOfData);
Thanks to #Bolling idea
I have implement to support to avoid any nullable from List
public void setList(ArrayList<ThisIsAdapterListObject> _newList) {
//get the current items
if (ThisIsAdapterList != null) {
int currentSize = ThisIsAdapterList.size();
ThisIsAdapterList.clear();
//tell the recycler view that all the old items are gone
notifyItemRangeRemoved(0, currentSize);
}
if (_newList != null) {
if (ThisIsAdapterList == null) {
ThisIsAdapterList = new ArrayList<ThisIsAdapterListObject>();
}
ThisIsAdapterList.addAll(_newList);
//tell the recycler view how many new items we added
notifyItemRangeInserted(0, _newList.size());
}
}
In my case I've had more then 5000 items in the list.
My problem was that when scrolling the recycler view, sometimes the "onBindViewHolder" get called while "myCustomAddItems" method is altering the list.
My solution was to add "synchronized (syncObject){}" to all the methods that alter the data list.
This way at any point at time only one method can read this list.
I got this error because I was mistakenly calling a method to remove a specific row from my recyclerview multiple times. I had a method like:
void removeFriends() {
final int loc = data.indexOf(friendsView);
data.remove(friendsView);
notifyItemRemoved(loc);
}
I was accidentally calling this method three times instead of once, so the second time loc was -1 and the error was given when it tried to remove it. The two fixes were to ensure the method was only called once, and also to add a sanity check like this:
void removeFriends() {
final int loc = data.indexOf(friendsView);
if (loc > -1) {
data.remove(friendsView);
notifyItemRemoved(loc);
}
}
I got the same problem and I have read that this happened in Samsung phones only...But the reality showed that this happens in a lot of brands.
After testing I realized that this happens only when you scroll fast the RecyclerView and then you go back either with the back button or the Up button. So I put inside Up button and onBackpressed the below snippet:
someList = new ArrayList<>();
mainRecyclerViewAdapter = new MainRecyclerViewAdapter(this, someList, this);
recyclerViewMain.setAdapter(mainRecyclerViewAdapter);
finish();
With this solution you just load a new Arraylist to the adapter and new adapter to recyclerView and then you finish activity.
Hope it helps someone
I got this error because i was calling "notifyItemInserted" twice by mistake.
In my case, adapter data changed. And i was wrongly use notifyItemInserted() for these changes. When i use notifyItemChanged, the error has gone away.
I am using a Cursor so I can not use the DiffUtils as proposed in the popular answers. In order to make it work for me I am disabling animations when the list is not idle. This is the extension that fixes this issue:
fun RecyclerView.executeSafely(func : () -> Unit) {
if (scrollState != RecyclerView.SCROLL_STATE_IDLE) {
val animator = itemAnimator
itemAnimator = null
func()
itemAnimator = animator
} else {
func()
}
}
Then you can update your adapter like that
list.executeSafely {
adapter.updateICursor(newCursor)
}
I ran into the same problem when I have update data while the RecyclerView is scrolling.
And I fixed it with the following solution:
Stop scroll our RecyclerView before update data.
Custom layout manager likes with the preceding answer.
Use DiffUtils to ensure updating data is correct.

Categories

Resources