1

I tried to implement the item selection feature in RecyclerView with the help of SelectionTracker but getting IllegalArgumentException and the stacktrace is not intuitive as it only shows the metadata.

this is how I am building the tracker

wordAdapter.tracker = new SelectionTracker.Builder<Long>(
                "mySelectionId",
                recyclerView,
                new StableIdKeyProvider(recyclerView),
                new MyDetailsLookUp(recyclerView),
                StorageStrategy.createLongStorage()
        ).withSelectionPredicate(
                SelectionPredicates.<Long>createSelectAnything()
        ).build();

MyDetailsLookUp class

class MyDetailsLookUp extends ItemDetailsLookup<Long> {
        RecyclerView recyclerView;

        MyDetailsLookUp(RecyclerView recyclerView) {
            this.recyclerView = recyclerView;
        }

        @Nullable
        @Override
        public ItemDetails<Long> getItemDetails(@NonNull MotionEvent e) {
            View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
            if (view != null) {
                //  getting individual view holders
                return ((WordAdapter.MyViewHolder) recyclerView.getChildViewHolder(view)).getItemDetails();
            }
            return null;
        }
    }

getItemDetails method in ViewHolder in the WordAdapter

ItemDetailsLookup.ItemDetails<Long> getItemDetails(){
            return new ItemDetailsLookup.ItemDetails<Long>(){
                @Override
                public int getPosition() {
                    return getAdapterPosition();
                }

                @Nullable
                @Override
                public Long getSelectionKey() {
                    return getItemId();
                }
            };
        }

Stacktrace 1

java.lang.IllegalArgumentException
        at androidx.core.util.Preconditions.checkArgument(Preconditions.java:38)
        at androidx.recyclerview.selection.DefaultSelectionTracker.anchorRange(DefaultSelectionTracker.java:269)
        at androidx.recyclerview.selection.MotionInputHandler.selectItem(MotionInputHandler.java:60)
        at androidx.recyclerview.selection.TouchInputHandler.onLongPress(TouchInputHandler.java:132)
        at androidx.recyclerview.selection.GestureRouter.onLongPress(GestureRouter.java:96)
        at android.view.GestureDetector.dispatchLongPress(GestureDetector.java:778)
        at android.view.GestureDetector.-wrap0(Unknown Source:0)
        at android.view.GestureDetector$GestureHandler.handleMessage(GestureDetector.java:293)
        at android.os.Handler.dispatchMessage(Handler.java:105)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6541)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)

Surprisingly, I have implemented the same code in Kotlin and it is working fine there. I found a SO post with the similar problem this, the accepted answer suggesting a custom ItemKeyProvider to be passed instead of StableIdKeyProvider. Therefore, on doing so hit me with this error.

Stacktrace 2

java.lang.IllegalStateException: Two different ViewHolders have the same stable ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT change.
     ViewHolder 1:ViewHolder{987472c position=1 id=-1, oldPos=-1, pLpos:-1} 
     View Holder 2:ViewHolder{e50e5f5 position=2 id=-1, oldPos=-1, pLpos:-1 not recyclable(1)} androidx.recyclerview.widget.RecyclerView{617d73 VFED..... .F....ID 42,42-1038,1542 #7f08007b app:id/recyclerview}, adapter:com.example.roomwordssample.WordAdapter@7ada430, layout:androidx.recyclerview.widget.GridLayoutManager@a15b8a9, context:com.example.roomwordssample.Main2Activity@ca1d275
        at androidx.recyclerview.widget.RecyclerView.handleMissingPreInfoForChangeError(RecyclerView.java:4058)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep3(RecyclerView.java:3982)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3652)
        at androidx.recyclerview.widget.RecyclerView.consumePendingUpdateOperations(RecyclerView.java:1877)
        at androidx.recyclerview.widget.RecyclerView$1.run(RecyclerView.java:407)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911)
        at android.view.Choreographer.doCallbacks(Choreographer.java:723)
        at android.view.Choreographer.doFrame(Choreographer.java:655)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897)
        at android.os.Handler.handleCallback(Handler.java:789)
        at android.os.Handler.dispatchMessage(Handler.java:98)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6541)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)

updated tracker

wordAdapter.tracker = new SelectionTracker.Builder<Long>(
                "mySelectionId",
                recyclerView,
               new GetItemDetails(recyclerView, ItemKeyProvider.SCOPE_MAPPED),
                new MyDetailsLookUp(recyclerView),
                StorageStrategy.createLongStorage()
        ).withSelectionPredicate(
                SelectionPredicates.<Long>createSelectAnything()
        ).build();

CustomItemKeyProvider

class CustomItemKeyProvider extends ItemKeyProvider<Long> {
        RecyclerView recyclerView;

        CustomItemKeyProvider(RecyclerView recyclerView, int scope) {
            super(scope);
            this.recyclerView = recyclerView;
        }

        @Nullable
        @Override
        public Long getKey(int position) {
            return wordAdapter.getItemId(position);
        }

        @Override
        public int getPosition(@NonNull Long key) {
            RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForItemId(key);
            return viewHolder == null ? RecyclerView.NO_POSITION : viewHolder.getLayoutPosition();
        }
    }

P.S: wordAdapter.setHasStableIds(true) is done before setting the adapter to the RecyclerView

Neeraj Sewani
  • 3,952
  • 6
  • 38
  • 55

1 Answers1

0

In your stacktrace:

java.lang.IllegalStateException: Two different ViewHolders have the same stable ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT change.
     ViewHolder 1:ViewHolder{987472c **position=1 id=-1**, oldPos=-1, pLpos:-1} 
     View Holder 2:ViewHolder{e50e5f5 **position=2 id=-1**, oldPos=-1, pLpos:-1 not recyclable(1)} 

In above error it says for position 1 and 2 in list, you are using same ID but it has to be unique. In order to use with selection Tracker, ensure you have unique id. Inform list that you have stable ID

setHasStableIds(true)

In RecyclerView.Adapter you need to sure you are using unique for each

public abstract class WordAdapter extends RecyclerView.Adapter {
  private List<Item> itemsList = new ArrayList<>();
  //other override methods

  @Override
    public long getItemId(int position) {
        return position; // here each value must be unique either position or data unique id
    }
}