javaandroidandroid-recyclerviewnotifydatasetchangedcontextual-action-bar

RecyclerView duplicating items after deletion from contextual action mode


In this part of my app, I am trying to implement deleting of selected favorite items via contextual action mode/bar, the problem is when I select an item then delete, it's deleted from the database and selected list but it is still available in recyclerView and it adds a duplicate from another item, the following gif clarify the problem

Edited the full adapter code FavoritesPostAdapter

public class FavoritesPostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private final FragmentActivity fragmentActivity;
    private final List<FavoritesEntity> favoritesList;
    private View rootView;

    private static final int CARD = 0;
    private static final int CARD_MAGAZINE = 1;
    private static final int TITLE = 2;
    private static final int GRID = 3;
    private static final int SDK_VERSION = Build.VERSION.SDK_INT;
    public static final String TAG = "POST ADAPTER";

    private int viewType;
    public final Fragment fragment;
    public final PostViewModel postViewModel;
    private ActionMode mActionMode;
    private boolean multiSelection = false;
//    private int selectedPostPosition ;
    private final List<FavoritesEntity> selectedPosts = new ArrayList<>();
    private final List<RecyclerView.ViewHolder> myViewHolders = new ArrayList<>();

    public FavoritesPostAdapter(FragmentActivity fragmentActivity,
                                List<FavoritesEntity> favoritesList, Fragment fragment,
                                PostViewModel postViewModel) {
        this.fragmentActivity = fragmentActivity;
        this.favoritesList = favoritesList;
        this.fragment = fragment;
        this.postViewModel = postViewModel;
    }

    public void setViewType(int viewType) {
        this.viewType = viewType;
        notifyDataSetChanged();
    }

    public int getViewType() {
        return this.viewType;
    }

    private final ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
        @Override
        public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
            mActionMode = actionMode;
            actionMode.getMenuInflater().inflate(R.menu.favorites_contextual_menu, menu);
            applyStatusBarColor(R.color.contextualStatusBarColor);
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
            return true;
        }

        @Override
        public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
            if (menuItem.getItemId() == R.id.delete_favorites_post) {
                for (FavoritesEntity favoritesEntity : selectedPosts) {
                    postViewModel.deleteFavoritePost(favoritesEntity);
                }
                showSnackBar(selectedPosts.size() + " post/s deleted");
                multiSelection = false;
                selectedPosts.clear();
                notifyDataSetChanged();
                mActionMode.finish();
            }
            return true;
        }

        @Override
        public void onDestroyActionMode(ActionMode actionMode) {

            for (RecyclerView.ViewHolder holder : myViewHolders) {
                changePostStyle(holder, R.color.cardBackgroundColor, R.color.strokeColor);
            }

            multiSelection = false;
            selectedPosts.clear();
            applyStatusBarColor(R.color.statusBarColor);
        }
    };

    private void showSnackBar(String message){
        Snackbar.make(rootView,message,Snackbar.LENGTH_SHORT).show();
    }

    private void applyStatusBarColor(int color) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            fragmentActivity.getWindow().setStatusBarColor(ContextCompat.getColor(fragmentActivity, color));
        }
    }

    private void applySelection(RecyclerView.ViewHolder holder, FavoritesEntity currentSelectedPost) {

        if (selectedPosts.contains(currentSelectedPost)) {
            selectedPosts.remove(currentSelectedPost);
            changePostStyle(holder, R.color.cardBackgroundColor, R.color.strokeColor);
            applyActionModeTitle();
        } else {
            selectedPosts.add(currentSelectedPost);
            changePostStyle(holder, R.color.cardBackgroundLightColor, R.color.primaryColor);
            applyActionModeTitle();
        }
    }

    private void changePostStyle(RecyclerView.ViewHolder holder, int backgroundColor, int strokeColor) {
        if (holder instanceof CardViewHolder) {
            ((CardViewHolder) holder).cardLayoutBinding.mainLinearLayout.setBackgroundColor(
                    ContextCompat.getColor(fragmentActivity.getApplicationContext(),
                            backgroundColor)
            );
            ((CardViewHolder) holder).cardLayoutBinding.cardView.setStrokeColor(
                    strokeColor);
        }
    }

    private void applyActionModeTitle() {
        if (selectedPosts.size() == 0) {
            mActionMode.finish();
            multiSelection = false;
        } else if (selectedPosts.size() == 1) {
            mActionMode.setTitle(selectedPosts.size() + " item selected");
        } else {
            mActionMode.setTitle(selectedPosts.size() + " items selected");
        }
    }


    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(fragmentActivity);
        View view;

        if (this.viewType == CARD) {
            final CardLayoutBinding cardLayoutBinding
                    = CardLayoutBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
            return new FavoritesPostAdapter.CardViewHolder(cardLayoutBinding);
        } else if (this.viewType == CARD_MAGAZINE) {
            final CardMagazineBinding cardMagazineBinding
                    = CardMagazineBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
            return new FavoritesPostAdapter.CardMagazineViewHolder(cardMagazineBinding);
        } else if (this.viewType == TITLE) {
            if (SDK_VERSION < Build.VERSION_CODES.LOLLIPOP) {
                view = inflater.inflate(R.layout.title_layout_v15, parent, false);
            } else {
                view = inflater.inflate(R.layout.title_layout, parent, false);
            }
            return new FavoritesPostAdapter.TitleViewHolder(view);
        } else {
            if (SDK_VERSION < Build.VERSION_CODES.LOLLIPOP) {
                view = inflater.inflate(R.layout.grid_layout_v15, parent, false);
            } else {
                view = inflater.inflate(R.layout.grid_layout, parent, false);
            }
            return new FavoritesPostAdapter.GridViewHolder(view);
        }

    }


    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {

        myViewHolders.add(holder);
        rootView = holder.itemView.getRootView();
//        selectedPostPosition = position;

        int itemType = getViewType();
        FavoritesEntity favoriteItem = favoritesList.get(position);
        final Document document = Jsoup.parse(favoriteItem.getItem().getContent());
        final Elements elements = document.select("img");

//        Log.e("IMAGE", document.getAllElements().select("img").get(0).attr("src"));


        switch (itemType) {
            case CARD:
                if (holder instanceof FavoritesPostAdapter.CardViewHolder) {
                    ((FavoritesPostAdapter.CardViewHolder) holder).bind(favoriteItem);

                    ((CardViewHolder) holder).cardLayoutBinding.cardView.setOnClickListener(view -> {
                                if (multiSelection) {
                                    applySelection(holder, favoriteItem);
                                } else {
                                    mActionMode.finish();
                                    if (Objects.requireNonNull(Navigation.findNavController(
                                            view
                                    ).getCurrentDestination()).getId() == R.id.nav_favorites) {
                                        Navigation.findNavController(view)
                                                .navigate(FavoritesFragmentDirections
                                                        .actionFavoritesFragmentToDetailsFragment(favoriteItem.getItem()));
                                    }
                                }
                            }
                    );
                    ((CardViewHolder) holder).cardLayoutBinding.cardView.setOnLongClickListener(view -> {
                        if (!multiSelection) {
                            multiSelection = true;
                            fragmentActivity.startActionMode(mActionModeCallback);
                            applySelection(holder, favoriteItem);
                            return true;
                        } else {
                            applySelection(holder, favoriteItem);
                            return true;
                        }

                    });
                }

                break;

            case CARD_MAGAZINE:
                if (holder instanceof FavoritesPostAdapter.CardMagazineViewHolder) {
                    FavoritesPostAdapter.CardMagazineViewHolder
                            cardMagazineViewHolder = (FavoritesPostAdapter.CardMagazineViewHolder) holder;
                    cardMagazineViewHolder.bind(favoriteItem);

                }
                break;
            case TITLE:
                if (holder instanceof FavoritesPostAdapter.TitleViewHolder) {
                    FavoritesPostAdapter.TitleViewHolder titleViewHolder = (FavoritesPostAdapter.TitleViewHolder) holder;
                    titleViewHolder.postTitle.setText(favoriteItem.getItem().getTitle());

                    Log.d("TITLE", "title layout called");


                    try {
                        Log.e("IMAGE", elements.get(0).attr("src"));
                        Glide.with(fragmentActivity).load(elements.get(0).attr("src"))
                                .transition(DrawableTransitionOptions.withCrossFade(600))
                                .placeholder(R.drawable.loading_animation)
                                .error(R.drawable.no_image)
                                .into(titleViewHolder.postImage);
                    } catch (IndexOutOfBoundsException e) {
                        titleViewHolder.postImage.setImageResource(R.drawable.no_image);
                        Log.e(TAG, e.toString());
                    }

                    if (position == getItemCount() - 1)
                        if (fragment instanceof HomeFragment) {
                            postViewModel.getPosts();
                        } else {
                            postViewModel.getPostListByLabel();
                        }

                }
                break;
            case GRID:
                if (holder instanceof FavoritesPostAdapter.GridViewHolder) {
                    FavoritesPostAdapter.GridViewHolder gridViewHolder = (FavoritesPostAdapter.GridViewHolder) holder;
                    gridViewHolder.postTitle.setText(favoriteItem.getItem().getTitle());


                    try {
                        Log.e("IMAGE", elements.get(0).attr("src"));
                        Glide.with(fragmentActivity).load(elements.get(0).attr("src"))
                                .transition(DrawableTransitionOptions.withCrossFade(600))
                                .placeholder(R.drawable.loading_animation)
                                .error(R.drawable.no_image)
                                .into(gridViewHolder.postImage);
                    } catch (IndexOutOfBoundsException e) {
                        gridViewHolder.postImage.setImageResource(R.drawable.no_image);
                        Log.e(TAG, e.toString());
                    }

                    if (position == getItemCount() - 1)
                        if (fragment instanceof HomeFragment) {
                            postViewModel.getPosts();
                        } else {
                            postViewModel.getPostListByLabel();
                        }

                }
        }


    }


    @Override
    public int getItemCount() {
        return favoritesList.size();
    }

    @Override
    public void setHasStableIds(boolean hasStableIds) {
        super.setHasStableIds(hasStableIds);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }


    public static class CardViewHolder extends RecyclerView.ViewHolder {

        final CardLayoutBinding cardLayoutBinding;
        final Context context;

        private CardViewHolder(final CardLayoutBinding binding) {
            super(binding.getRoot());
            cardLayoutBinding = binding;
            context = cardLayoutBinding.getRoot().getContext();


        }

        private void bind(FavoritesEntity favoriteItem) {
            final Document document = Jsoup.parse(favoriteItem.getItem().getContent());
            final Elements elements = document.select("img");

//        Log.e("IMAGE", document.getAllElements().select("img").get(0).attr("src"));

            Date date = new Date();
            SimpleDateFormat format = new SimpleDateFormat
                    ("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());

            cardLayoutBinding.postTitle.setText(favoriteItem.getItem().getTitle());

            try {
                Log.e("IMAGE", elements.get(0).attr("src"));
                Glide.with(context).load(elements.get(0).attr("src"))
                        .transition(DrawableTransitionOptions.withCrossFade(600))
                        .placeholder(R.drawable.loading_animation)
                        .error(R.drawable.no_image)
                        .into(cardLayoutBinding.postImage);
            } catch (IndexOutOfBoundsException e) {
                cardLayoutBinding.postImage.setImageResource(R.drawable.no_image);
                Log.e(TAG, e.toString());
            }


            cardLayoutBinding.postDescription.setText(document.text());
            try {
                date = format.parse(favoriteItem.getItem().getPublished());

            } catch (ParseException e) {
                e.printStackTrace();
            }

            PrettyTime prettyTime = new PrettyTime();

            cardLayoutBinding.postDate.setText(prettyTime.format(date));

        }
    }

    public static class CardMagazineViewHolder extends RecyclerView.ViewHolder {

        final CardMagazineBinding cardMagazineBinding;
        final Context context;

        private CardMagazineViewHolder(final CardMagazineBinding binding) {
            super(binding.getRoot());
            cardMagazineBinding = binding;
            context = cardMagazineBinding.getRoot().getContext();


        }

        private void bind(FavoritesEntity favoriteItem) {
            final Document document = Jsoup.parse(favoriteItem.getItem().getContent());
            final Elements elements = document.select("img");


            Date date = new Date();
            SimpleDateFormat format = new SimpleDateFormat
                    ("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());


//        Log.e("IMAGE", document.getAllElements().select("img").get(0).attr("src"));
            cardMagazineBinding.postTitle.setText(favoriteItem.getItem().getTitle());


            try {
                Log.e("IMAGE", elements.get(0).attr("src"));
                Glide.with(context).load(elements.get(0).attr("src"))
                        .transition(DrawableTransitionOptions.withCrossFade(600))
                        .placeholder(R.drawable.loading_animation)
                        .error(R.drawable.no_image)
                        .into(cardMagazineBinding.postImage);
            } catch (IndexOutOfBoundsException e) {
                cardMagazineBinding.postImage.setImageResource(R.drawable.no_image);
                Log.e(TAG, e.toString());
            }

            try {
                date = format.parse(favoriteItem.getItem().getPublished());

            } catch (ParseException e) {
                e.printStackTrace();
            }
            PrettyTime prettyTime = new PrettyTime();

            cardMagazineBinding.postDate.setText(prettyTime.format(date));

        }
    }

    public static class TitleViewHolder extends RecyclerView.ViewHolder {
        TextView postTitle;
        com.blogspot.abtallaldigital.utils.MyImageview postImage;


        private TitleViewHolder(@NonNull View itemView) {
            super(itemView);
            postTitle = itemView.findViewById(R.id.postTitle);
            postImage = itemView.findViewById(R.id.postImage);
        }
    }


    public static class GridViewHolder extends RecyclerView.ViewHolder {
        TextView postTitle;
        MyImageview postImage;


        private GridViewHolder(@NonNull View itemView) {
            super(itemView);
            postTitle = itemView.findViewById(R.id.postTitle);
            postImage = itemView.findViewById(R.id.postImage);
        }
    }
}

favorites_contextual_menu xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/delete_favorites_post"
        android:title="@string/delete_post"
        app:showAsAction="ifRoom"
        app:iconTint="@color/white"
        android:icon="@drawable/ic_delete"
        >

    </item>

</menu>

PS: I tried to get the selected position from the holder and assign it to selectedPostPosition int value to use it in notifyItemRemoved(position); and notifyItemRangeChanged(position, getItemCount()); like in this answer but it doesn't fix the issue


Solution

  • The problem was happening because variable

    private List<FavoritesEntity> favoritesList;
    

    inside FavouritesFragment which kept all previous objects even after deleting items from a local database.

    On every delete event, you were getting updates about database change -> postViewModel.metal favorites().observe, where you called addAll to this reference which contained all previous items.

    Solution: https://github.com/dautovicharis/DummyApp2/commit/752a48fc98761d53c6a8f72076489a0f68ee348b

    Source: https://github.com/dautovicharis/DummyApp2