简体   繁体   中英

How can I avoid "Inconsistency detected. Invalid item position" when updating Realm from separate thread?

UPDATE:

I see the same error ("Inconsistency detected. Invalid view holder adapter position") in another situation - this time when bulk adding.

The situation is I am implementing a nested recyclerview, each of which uses a RealmRecyclerViewAdapter and each has an OrderedRealmCollection as its basis. The result I'm going after is this:

在此处输入图片说明

I have implemented this at the first level by a query for distinct items in my realm keyed off of year and month:

OrderedRealmCollection<Media> monthMedias = InTouchDataMgr.get().getDistinctMedias(null,new String[]{Media.MEDIA_SELECT_YEAR,Media.MEDIA_SELECT_MONTH});

This gives me one entry for July, one for August, of 2019 in this example.

Then for each ViewHolder in that list, during the bind phase I make another query to determine how many Media items are in each month for that year:

    void bindItem(Media media) {
        this.media = media;
        // Get all the images associated with the year in that date, set adapter in recyclerview
        OrderedRealmCollection<Media> medias = InTouchDataMgr.get().getAllMediasForYearAndMonth(null, media.getYear(), media.getMonth());

        // This adapter loads the CardView's recyclerView with a StaggeredGridLayoutManager
        int minSize = Math.min(MAX_THUMBNAILS_PER_MONTH_CARDVIEW, medias.size());
        imageRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(minSize >= 3 ? 3 : Math.max(minSize, 1), LinearLayoutManager.VERTICAL));
        imageRecyclerView.setAdapter(new RealmCardViewMediaAdapter(medias, MAX_THUMBNAILS_PER_MONTH_CARDVIEW));
    }

At this point I have the single month which is bound to the first ViewHolder, and now I have the count of media for that month, and I want to cause this ViewHolder to display a sampling of those items (maximum of MAX_THUMBNAILS_PER_MONTH_CARDVIEW which is initialized as 5) with the full count shown in the header.

So I pass the full OrderedRealmCollection of that media to the "second level" adapter that handles the list for this CardView.

That adapter looks like this:

private class RealmCardViewMediaAdapter extends RealmRecyclerViewAdapter<Media, CardViewMediaHolder> {
    int forcedCount = NO_FORCED_COUNT;
    RealmCardViewMediaAdapter(OrderedRealmCollection<Media> data, int forcedCount) {
        super(data, true);
        this.forcedCount = forcedCount;
    }

    @NonNull
    @Override
    public CardViewMediaHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater layoutInflater = LayoutInflater.from(InTouch.getInstance().getApplicationContext());
        View view = layoutInflater.inflate(R.layout.timeline_recycler_row_content, parent, false);
        return new CardViewMediaHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull CardViewMediaHolder holder, int position) {
        // Let Glide load the thumbnail
        GlideApp.with(InTouch.getInstance().getApplicationContext())
                .load(Objects.requireNonNull(getData()).get(position).getUriPathToMedia())
                .thumbnail(0.05f)
                .placeholder(InTouchUtils.getProgressDrawable())
                .error(R.drawable.ic_image_error)
                .into(holder.mMediaImageView);
    }

    @Override
    public int getItemCount() {
        //TODO - the below attempts to keep the item count at forced count when so specified, but this is causing
        //       "Inconsistency detected. Invalid view holder adapter position" exceptions when adding a bulk number of images
        return (forcedCount == NO_FORCED_COUNT ? getData().size() : Math.min(forcedCount,getData().size()));
        //return getData().size();
    }
}

So what this is attempting to do is limit the number of items reported by the adapter to the smaller set of thumbnails to show in the first level CardView to a max of 5, spread around using that StaggeredGridLayout.

All this works perfectly until I do a bulk add from another thread. The use case is the user has selected the FAB to add images, and they have selected a bunch (my test was ~250). Then the Uri for all of this is passed to a thread, which does a callback into the method below:

public void handleMediaCreateRequest(ArrayList<Uri> mediaUris, String listId) {
    if ( handlingAutoAddRequest) {
        // This will only be done a single time when in autoAdd mode, so clear it here
        // then add to it below
        autoAddedIDs.clear();
    }
    // This method called from a thread, so different realm needed.
    Realm threadedRealm = InTouchDataMgr.get().getRealm();
    try {
        // For each mediaPath, create a new Media and add it to the Realm
        int x = 0;
        for ( Uri uri: mediaUris) {
            try {
                Media media = new Media();
                InTouchUtils.populateMediaFromUri(this, media, uri);
                InTouchDataMgr.get().addMedia(media, STATUS_UNKNOWN, threadedRealm);
                autoAddedIDs.add(media.getId());
                if ( x > 2) {
                    // Let user see what is going on
                    runOnUiThread(this::updateUI);
                    x = 0;
                }
                x++;
            } catch (Exception e) {
                Timber.e("Error creating new media in a batch, uri was %s, error was %s", uri.getPath(),e.getMessage());
            }
        }
    } finally {
        InTouchDataMgr.get().closeRealmSafely(threadedRealm);
        runOnUiThread(this::updateUI);
    }
}

This method is operating against the realm, which is then making its normal callback into the OrderedCollection which is the base of the list in the recyclerview(s).

The addMedia() method is standard Realm activity, and works fine everywhere else.

updateUI() essentially causes an adapter.notifyDataSetChanged() call, in this case to the RealmCardViewMediaAdapter.

If I either don't use a separate thread, or I don't attempt to limit the number of items the adapter returns to a max of 5 items, then this all works perfectly.

If I leave the limit of 5 in as the return value from getItemCount() and don't refresh the UI until all has been added, then this also works, even from a different thread.

So it seems there is something about notifyDataSetChanged() being called as the Realm based list of managed objects is being updated in real time that is generating this error. But I don't know why or how to fix?

UPDATE END


I am using Realm Java DB 6.0.2 and realm:android-adapters:3.1.0

I created a class that extends RealmRecyclerViewAdapter class for my RecyclerView:

class ItemViewAdapter extends RealmRecyclerViewAdapter<RealmObject, BindableViewHolder> implements Filterable {

        ItemViewAdapter(OrderedRealmCollection data) {
            super(data, true);
        }

I am initializing this adapter using the standard pattern of passing an OrderedRealmCollection to the adapter:

ItemViewAdapter createItemAdapter() {
    return new ItemViewAdapter(realm.where(Contact.class).sort("displayName"));
}

"realm" has been previously initialized in the class creating the adapter.

I allow the user to identify one or more rows in this recyclerView that they want to delete, then I execute an AsyncTask which calls the method handling the delete:

public static class DoHandleMultiDeleteFromAlertTask extends AsyncTask {
    private final WeakReference<ListActivity> listActivity;
    private final ActionMode mode;

    DoHandleMultiDeleteFromAlertTask(ListActivity listActivity, ActionMode mode) {
        this.listActivity = new WeakReference<>(listActivity);
        this.mode = mode;
    }

    @Override
    protected void onPreExecute() {
        listActivity.get().mProgressBar.setVisibility(View.VISIBLE);
    }

    @Override
    protected void onPostExecute(Object o) {
        // Cause multi-select to end and selected map to clear
        mode.finish();
        listActivity.get().mProgressBar.setVisibility(View.GONE);
        listActivity.get().updateUI(); // Calls a notifyDataSetChanged() call on the adapter

    }

    @Override
    protected Object doInBackground(Object[] objects) {
        // Cause deletion to happen.
        listActivity.get().handleMultiItemDeleteFromAlert();
        return null;
    }
}

Inside handleMultiItemDeleteFromAlert(), since we are being called from a different thread I create and close a Realm instance to do the delete work:

void handleMultiItemDeleteFromAlert() {
    Realm handleDeleteRealm = InTouchDataMgr.get().getRealm();
    try {
        String contactId;
        ArrayList<String> contactIds = new ArrayList<>();
        for (String key : mSelectedPositions.keySet()) {
            // The key finds the Contact ID to delete
            contactId = mSelectedPositions.getString(key);
            if (contactId != null) {
                contactIds.add(contactId);
            }
        }
        // Since we are running this from the non-UI thread, I pass a runnable that will
        // Update the UI every 3rd delete to give the use some sense of activity happening.
        InTouchDataMgr.get().deleteContact(contactIds, handleDeleteRealm, () -> runOnUiThread(ContactListActivity.this::updateUI));

    } finally {
        InTouchDataMgr.get().closeRealmSafely(handleDeleteRealm);
    }
}

And the deleteContact() method looks like this:

public void deleteContact(ArrayList<String> contactIds, Realm realm, Runnable UIRefreshRunnable) {

    boolean success = false;

    try {
        realm.beginTransaction();
        int x = 0;
        for ( String contactId : contactIds ) {
            Contact c = getContact(contactId, realm);
            if (c == null) {
                continue;
            }

            // Delete from the realm
            c.deleteFromRealm();

            if ( UIRefreshRunnable != null && x > 2 ) {
                try {
                    UIRefreshRunnable.run();
                } catch (Exception e) {
                    //No-op
                }
                x = 0;
            }
            x++;
        }
        success = true;
    } catch (Exception e) {
        Timber.d("Exception deleting contact from realm: %s", e.getMessage());
    } finally {
        if (success) {
            realm.commitTransaction();
        } else {
            realm.cancelTransaction();
        }
    }

Now my problem - when I was doing this work entirely from the UI thread I had no errors. But now when the transaction is committed I am getting:

Inconsistency detected. Invalid item position 1(offset:-1).state:5 androidx.recyclerview.widget.RecyclerView{f6a65cc VFED..... .F....ID 0,0-1440,2240 #7f090158 app:id/list_recycler_view}, adapter:com.reddragon.intouch.ui.ListActivity$ItemViewAdapter@5d77178,

<a bunch of other lines here>

I thought that RealmRecyclerViewAdapter already registered listeners, kept everything straight, etc. What more do I need to do?

The reason I am using a separate thread here is that if a user identifies a few dozen (or perhaps hundreds) items in the list to delete, it can take several seconds to perform the delete (depending on which list we are talking about - there are various checks and other updates that have to happen to preferences, etc.), and I didn't want the UI locked during this process.

How is the adapter getting "inconsistent"?

I solved this by adjusting the architecture slightly. It seems there is some issue with StaggeredGridLayoutManager when mixed with:

  1. The dynamic ability of RealmRecyclerView to update itself automatically during bulk adds or deletes.
  2. An adapter that attempts to limit what is shown by returning a count from getItemCount() that is not equal to the current list count.

I suspect this has to do with how ViewHolder instances get created and positioned by the layout manager, since this is where the error is pointing to.

So what I did instead is rather than have the adapter return a value that can be less than the actual count of the list being managed at any point in time, I changed the realm query to use the .limit() capability. Even when the query returns less than the limit initially, it has the nice side effect of capping itself at the limit requested as the list dynamically grows from the bulk add. And it has the benefit of allowing getItemCount() to return whatever the current size of that list is (which always works).

To recap - when in "Month View" (where I want the user to only see a max of 5 images like the screen shot above), the first step is to populate the adapter of the top level RealmRecyclerView with the result of a DISTINCT type query which results in an OrderedRealmCollection of Media objects which correspond to each month from each year in my media Library.

Then in the "bind" flow of that adapter, the MonthViewHolder performs the second realm query, this time with a limit() clause:

        OrderedRealmCollection<Media> medias = InTouchDataMgr.get().getMediasForYearAndMonthWithLimit(null,
                media.getYear(),
                media.getMonth(),
                MAX_THUMBNAILS_PER_MONTH_CARDVIEW); // Limit the results to our max per month

Then the adapter for the RealmRecyclerView associated with this specific month uses the results of this query as its list to manage.

Now it can return getData().size() as the result of the getItemCount() call whether I am in the Month view (capped by limit() ) or in my week view which returns all media items for that week and year.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM