4

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

Dr Mido
  • 2,414
  • 4
  • 32
  • 72
  • Apart from the issue, the `getItemId()` need to be overriden to have unique IDs as `setHasStableIds()` requires that to work – Zain Nov 03 '21 at 21:11
  • @zain I have the same project structure but in kotlin without `setHasStableIds` do you think this method causes the issue? – Dr Mido Nov 03 '21 at 21:44
  • ps: I already override this two methods – Dr Mido Nov 03 '21 at 21:52
  • Not totally sure, but they can be related to the duplicating item; but I think they are not not related to the deletion .. Can you please remove them for a while and see – Zain Nov 03 '21 at 21:55
  • Do you `setHasStableIds()` to true? – Zain Nov 03 '21 at 21:58
  • @DrMido If you can create simple demo with this problem on Github, then I can help with debugging. – Haris Nov 04 '21 at 09:12
  • @zain I did not add this line `favoritesPostAdapter.setHasStableIds(true);`, but after adding it there's no effect, I also tried to delete both two methods, the problem still remain – Dr Mido Nov 04 '21 at 10:27
  • @haris-dautović I was intending to push the whole project after solving this problem, I will wait a while if someone knows the cause of this problem or maybe something wrong I added – Dr Mido Nov 04 '21 at 10:30
  • `onBindViewHolder()` is suspect. Can you post all the code for the adapter? – Cheticamp Nov 04 '21 at 14:00
  • @cheticamp I edited it you can check now – Dr Mido Nov 04 '21 at 14:13
  • For each switch statement in `onBindViewHolder()` you do a check for `instance of ...`. Either these statements are not needed (if they are always true) or you will end up with unbound view holders (if any of the statements is false.) This may or may not be the problem, but it is something to check out. – Cheticamp Nov 04 '21 at 14:19
  • @cheticamp it will never be always true because I have 4 different holders, `CardViewHolder`, `CardMagazineViewHolder`, `TitleViewHolder`, and `GridViewHolder`, the user can switch between them, to understand this case more you can check this my old [question](https://stackoverflow.com/questions/55171778/switch-between-recyclerview-layouts-when-click-on-alertdialog-item-list) , currently, I implement the contextual action mode on `CardViewHolder` `cardView.setOnClickListener` only and I plan to apply it to others, but after solving this problem, do you say If I moved this code to outside... – Dr Mido Nov 04 '21 at 14:38
  • the switch case the problem may resolve?! – Dr Mido Nov 04 '21 at 14:38
  • If your code drops through and none of the switch statements is executed then your view holder will have stale data, so it will look like that deleted data is still there but it isn't really. I am not clear on your reasoning, but you will need to change your logic. The view type is the definitive answer to what kind of view holder you have (or it should be.) – Cheticamp Nov 04 '21 at 14:45
  • I am also questioning the way the view type is handled in `setViewType` and `getViewType`. – Cheticamp Nov 04 '21 at 14:48
  • @cheticamp I tried to delete all other holders and both methods of set & get viewType, and deleted the switch and make only one return but this all didn't resolve the problem – Dr Mido Nov 04 '21 at 15:23
  • It's somewhere in the handling of the view holders. Delete an item and walk through the adapter with the debugger to make sure what you think is happening is what is actually happening. – Cheticamp Nov 04 '21 at 15:38
  • When you click the remove button, you must to notify your viewModel, and the viewmodel can remove from database and must be update the ui again, updating the livedata and the recycler doing the notifyDataSetChanged with favoritesList – Manuel Mato Nov 05 '21 at 21:33
  • @ManuelMato it's already in the code check `onActionItemClicked` – Dr Mido Nov 06 '21 at 13:23
  • @manuel-mato, unfortunately, I can't understand Spanish, but I translated your replay to English, I tried to update the ViewModel method `deleteFavoritePost` to call `getAllFavorites` after it, and I tried also to call remove(object) in the ViewModel but the same issue, I'll push the app in the github If the problem still remains in the next 3 days – Dr Mido Nov 07 '21 at 14:19
  • Try removing else condition in applySelection method? seems adding new item if not present. – TRK P Nov 09 '21 at 10:57
  • @trk-p This is responsible for add selected item to list and changing the selected item style, I tried to remove it, but when I long click there's no item selected to delete it – Dr Mido Nov 09 '21 at 16:21
  • @DrMido Thanks - I'll do it right now :) – Haris Nov 09 '21 at 19:20
  • @DrMido Currently I'm missing private static final String KEY = BuildConfig.BLOGGER_KEY;. If you can create one for me it will be great. I think it's not good to post it here, so you can send me an email which you can find in my profile. – Haris Nov 09 '21 at 19:44
  • @haris-dautović I used [Secrets Gradle Plugin for Android](https://github.com/google/secrets-gradle-plugin), so it's hidden and if you clone the project it will work normally, I think you don't need the key for the current case – Dr Mido Nov 09 '21 at 19:59
  • @DrMido As you can see there was too much ping-pong in previous comments to solve this problem. In order to try this on my side, I'll require a real case without mock or additional modification to make the app work without API. If you can provide DEV_API key I will be glad to try to help you with this problem. Thanks – Haris Nov 09 '21 at 20:15
  • @haris-dautović NP it's just dummy app :D, I sent you the key – Dr Mido Nov 09 '21 at 20:36

2 Answers2

2

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

Haris
  • 4,130
  • 3
  • 30
  • 47
0

Instead of using notifyItemRangeChanged(position, getItemCount());,

you can use

notifyItemRemoved(position);

this is because notifyItemRangeChanged(position, getItemCount()); does not remove the item like your requirements!

Sambhav Khandelwal
  • 3,585
  • 2
  • 7
  • 38