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