java - 如何避免“检测到不一致。从单独的线程更新Realm时,“项目位置无效”?

标签 java android realm

更新:

我在另一种情况下看到了相同的错误(“检测到不一致。无效的视图支架适配器位置”),这是批量添加时。

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

enter image description here

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

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


在此示例中,这给了我7月一个条目,2019年8月一个条目。

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

    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)完整的计数显示在标题中。

因此,我将该媒体的完整OrderedRealmCollection传递给处理此CardView列表的“第二级”适配器。

该适配器如下所示:

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中列表的基础。

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

updateUI()本质上会导致对RealmCardViewMediaAdapter的adapter.notifyDataSetChanged()调用。

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

如果我将5的限制保留为getItemCount()的返回值,并且在所有内容都添加完之前不刷新UI,那么即使从其他线程执行此操作也可以。

因此似乎有一些关于notifyDataSetChanged()被调用的信息,因为基于领域的托管对象列表正在实时更新,从而产生此错误。但是我不知道为什么或如何解决?

更新结束



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

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

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


先前已在创建适配器的类中初始化了“领域”。

我允许用户在此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。

适配器如何变得“不一致”?

最佳答案

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


RealmRecyclerView的动态功能可以在批量添加或删除过程中自动更新。
一个适配器,尝试通过从getItemCount()返回一个不等于当前列表计数的计数来限制显示的内容。


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

因此,我做的不是让适配器返回的值可以小于在任何时间点管理的列表的实际计数,而是更改了领域查询以使用.limit()功能。即使查询最初返回的值小于限制,它也会产生很好的副作用,因为列表会随着批量添加的增长而动态增长,从而将自身限制在请求的限制。而且它的好处是允许getItemCount()返回该列表的当前大小(始终有效)。

回顾一下-在“月视图”中(我希望用户最多只能看到5张图像,如上面的屏幕截图所示),第一步是使用DISTINCT类型的结果填充顶级RealmRecyclerView的适配器查询会导致Media对象的OrderedRealmCollection,该对象对应于我的媒体库中每年的每个月。

然后,在该适配器的“绑定”流程中,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适配器使用此查询的结果作为要管理的列表。

现在,无论我是在Month视图(由limit()限制)还是在Week视图中,它都可以返回getData()。size()作为getItemCount()调用的结果,该视图返回该周和年份的所有媒体项目。

关于java - 如何避免“检测到不一致。从单独的线程更新Realm时,“项目位置无效”?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60547852/

相关文章:

java - 使用 Retrofit/Rxjava 进行数据绑定(bind)/实时数据处理配置更改

java - JSON 字符串到对象的映射

javascript - Android 从 WebView Javascript 访问本地文件

java - 合并多个 RealmList 并对结果列表进行排序?

android - Realm.io Android 从表中获取最后 20 项的最佳方法

arrays - 是否可以在 Realm 中将数组转换为 Result<>?

java - jar文件被执行

java - 为什么要创建 List<Object>

java - Facebook 归因 ID,它是什么?

android - 当前位置 GPS,程序意外关闭