从单独的线程更新 Realm 时如何避免 "Inconsistency detected. Invalid item position"?

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

更新:

我在另一种情况下看到了同样的错误 ("Inconsistency detected. Invalid view holder adapter position") - 这次是批量添加。

情况是我正在实现一个嵌套的 recyclerview,每个都使用一个 RealmRecyclerViewAdapter 并且每个都有一个 OrderedRealmCollection 作为其基础。我追求的结果是这样的:

我已经在第一级通过查询我领域中的不同项目来实现这一点,这些项目以年和月为键:

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

在此示例中,这为我提供了 2019 年 7 月的一项,8 月的一项。

然后对于该列表中的每个 ViewHolder,在绑定阶段我进行另一个查询以确定该年每个月有多少媒体项目:

    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));
    }

此时我有一个绑定到第一个 ViewHolder 的月份,现在我有那个月的媒体数,我想让这个 ViewHolder 显示这些项目的样本(最多MAX_THUMBNAILS_PER_MONTH_CARDVIEW 初始化为 5),完整计数显示在 header.

所以我将该媒体的完整 OrderedRealmCollection 传递给处理此 CardView 列表的 "second level" 适配器。

那个适配器看起来像这样:

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();
    }
}

所以这是试图做的是将适配器报告的项目数量限制为较小的一组缩略图,以显示在第一级 CardView 中,最多 5 个,使用 StaggeredGridLayout 分布。

在我从另一个线程进行批量添加之前,所有这一切都完美无缺。用例是用户选择了 FAB 来添加图像,他们选择了一堆(我的测试是 ~250)。然后将所有这些的 Uri 传递给一个线程,该线程对以下方法进行回调:

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);
    }
}

此方法针对领域进行操作,领域随后将其正常回调到 OrderedCollection 中,这是 recyclerview(s) 中列表的基础。

addMedia() 方法是标准 Realm activity,在其他任何地方都可以正常工作。

updateUI() 本质上会导致 adapter.notifyDataSetChanged() 调用,在本例中是调用 RealmCardViewMediaAdapter。

如果我不使用单独的线程,或者我不尝试将适配器 return 的项目数量限制为最多 5 个项目,那么这一切都可以正常工作。

如果我将 5 的限制保留为 getItemCount() 的 return 值并且在添加所有内容之前不刷新 UI,那么这也有效,即使是从不同的线程。

所以似乎有一些关于 notifyDataSetChanged() 被调用的事情,因为托管 objects 的基于领域的列表正在实时更新,这会产生这个错误。但我不知道为什么或如何解决?

更新结束


我正在使用 Realm Java DB 6.0.2 和 realm:android-adapters:3.1.0

我为我的 RecyclerView 创建了一个扩展 RealmRecyclerViewAdapter class 的 class:

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

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

我正在使用将 OrderedRealmCollection 传递给适配器的标准模式初始化此适配器:

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

"realm" 先前已在 class 创建适配器时初始化。

我允许用户在此 recyclerView 中识别他们想要删除的一行或多行,然后我执行一个调用处理删除的方法的 AsyncTask:

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;
    }
}

在 handleMultiItemDeleteFromAlert() 内部,因为我们是从不同的线程调用的,所以我创建并关闭了一个 Realm 实例来执行删除工作:

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);
    }
}

deleteContact() 方法如下所示:

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();
        }
    }

现在是我的问题 - 当我完全从 UI 线程完成这项工作时,我没有出现任何错误。但是现在当事务提交时我得到:

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>

我以为 RealmRecyclerViewAdapter 已经注册了监听器,一切都保持正常等等。我还需要做什么?

我在这里使用单独的线程的原因是,如果用户在列表中识别出几十个(或可能数百个)要删除的项目,执行删除可能需要几秒钟(取决于我们选择的列表正在谈论 - 必须对首选项进行各种检查和其他更新等),我不希望在此过程中锁定 UI。

适配器如何获得 "inconsistent"?

我通过稍微调整架构解决了这个问题。与 StaggeredGridLayoutManager 混合时似乎存在一些问题:

  1. RealmRecyclerView 在批量添加或删除期间自动更新自身的动态能力。
  2. 一个适配器试图通过 return 从 getItemCount() 获取一个不等于当前列表计数的计数来限制所显示的内容。

我怀疑这与布局管理器如何创建和定位 ViewHolder 实例有关,因为这是错误指向的地方。

所以我所做的不是让适配器 return 的值可以小于在任何时间点管理的列表的实际计数,我更改了领域查询以使用.limit() 能力。即使查询 return 最初小于限制,随着列表从批量添加动态增长,它也有很好的副作用,可以将自身限制在请求的限制。它的好处是允许 getItemCount() return 无论该列表的当前大小是多少(总是有效)。

回顾一下 - 在 "Month View" 中(我希望用户最多只能看到 5 张图像,如上面的屏幕截图),第一步是填充顶级 RealmRecyclerView 的适配器DISTINCT 类型查询的结果,该查询生成媒体对象的 OrderedRealmCollection,这些对象对应于我的媒体库中每年的每个月。

然后在该适配器的 "bind" 流程中,MonthViewHolder 执行第二个领域查询,这次使用 limit() 子句:

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

然后与这个特定月份关联的 RealmRecyclerView 的适配器使用这个查询的结果作为它的列表来管理。

现在它可以 return getData().size() 作为 getItemCount() 调用的结果,无论我是在月视图(由 limit() 限制)还是在我的周视图中 return那一周和那一年的所有媒体项目。