Java – Updating the StableIdKeyProvider cache and RecyclerView/SelectionTracker crashes on new selections after deleting items

Updating the StableIdKeyProvider cache and RecyclerView/SelectionTracker crashes on new selections after deleting items… here is a solution to the problem.

Updating the StableIdKeyProvider cache and RecyclerView/SelectionTracker crashes on new selections after deleting items

Preparation:

RecyclerView binds to RecyclerView.Adapter (bind) to SQLite Cursor (via ContentProvider & Loader). RecyclerView and RecyclerView.Adapter are linked with the SelectionTracker design suggests .
SelectionTracker is built using the StableIdKeyProvider.

Step 1 – Delete an item:

  1. Long press the item that selects RecyclerViews (toast to SelectionObserver from SelectionTracker), draw the action bar context menu, and fire
    Delete the Action to perform the SQL delete task.
  2. After the SQL deletion is completed, it is used
    as a Cursor Loader update
    restartLoader call.
  3. onLoadFinished fires to get a new Cursor, on
    The RecyclerView.Adapter method notifyDataSetChanged was called.
  4. RecyclerView.Adapter Redraws the RecyclerView content and it’s business as usual
    Good.

Step 2 – Select a different item. Crash:

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:779)
    at android.view.GestureDetector.access$200(GestureDetector.java:40)
    at android.view.GestureDetector$GestureHandler.handleMessage(GestureDetector.java:293)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loop(Looper.java:193)
    at android.app.ActivityThread.main(ActivityThread.java:6669)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

What I saw in the first step of the process of deleting the project.
When the StableIdKeyProvider does its internal work using the onDetached ViewHolder project, it does not see the previously assigned Location Adapter for ViewHolder:

   void onDetached(@NonNull View view) {
        RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
        int position = holder.getAdapterPosition();
        long id = holder.getItemId();
        if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {

int position Here is the RecyclerView.NO_POSITION

This is why RecyclerView crashes after the StableIdKeyProvider's cache contains old snapshots of IDs without deletion impact.

The question is – why? How do I update the cache for the StableIdKeyProvider?

Another note:
I saw this comment when I read the RecyclerView code:

     * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the
     * next layout pass, the return value of this method will be {#NO_POSITION}.

I don’t understand the exact meaning of this phrase. Maybe I’m running into the situation described – notifyDataSetChanged is called at an inappropriate time? Or do I need to call it twice?

Postscript.
The text description is embarrassing, there is a lot of complex code

Solution

I ended up playing with StableIdKeyProvider and switched to my own ItemKeyProvider implementation:

new ItemKeyProvider<Long>(ItemKeyProvider.SCOPE_MAPPED) {
                    @Override
                    public Long getKey(int position) {
                        return adapter.getItemId(position);
                    }

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

Crash is gone, and the navigation/selection/modification of RecyclerView looks fine.
What about StableIdKeyProvider? … Well, probably it’s not designed to handle the mutable content of RecyclerView.

Updated 2021-12-03

Last week I had a new fight on RecycleView.
As mentioned in the question – the exact problem is the cache of the StableIdKeyProvider. Switching to ItemKeyProvider is the workaround.
As the code for the StableIdKeyProvider explains, chache binds to window events: attach and detach. So, the comment I quoted above – exactly the problem: when a new Cursor arrives – reattach the Cursor to the Adapter and notify – it needs to fire at the right time. “Right time” – Schedule this job to the layout message thread. In this way, RecyclerView and the underlying “toolbox” can perform the update correctly on their own. To do this, simply provide a new Cursor in the post runnable method. Code:

@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
    recycler.post(new Runnable() {
        @Override
        public void run() {
            adapter.swapCursor(data);
        }
    });
...

Related Problems and Solutions