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:
- Long press the item that
selects RecyclerViews
(toast toSelectionObserver from SelectionTracker
), drawthe action bar context menu, and fire
Delete the Action to perform the SQL delete task. - After the SQL deletion is completed, it is used
as a Cursor Loader update
restartLoader
call. onLoadFinished
fires to get a newCursor
, on
TheRecyclerView.Adapter
methodnotifyDataSetChanged
was called.RecyclerView.Adapter
Redrawsthe 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);
}
});
...