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);
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.
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.