Как можно избежать «Обнаружена несогласованность. Неверная позиция элемента» при обновлении Realm из отдельного потока? - PullRequest
0 голосов
/ 05 марта 2020

ОБНОВЛЕНИЕ:

Я вижу ту же ошибку («Обнаружено несоответствие. Неверное положение адаптера держателя вида») в другой ситуации - на этот раз при массовом добавлении.

Ситуация такова, что я реализую вложенное представление переработчика, каждое из которых использует RealmRecyclerViewAdapter, и каждое имеет OrderedRealmCollection в качестве основы. Результат, который я получу после этого:

enter image description here

Я реализовал это на первом уровне с помощью запроса для отдельных элементов в моей области, отключенной года и месяца:

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

Это дает мне одну запись для июля, одну для августа, 2019 в этом примере.

Затем для каждого 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, которая является основой списка в обзоре (ях) реселлера.

Метод addMedia () является стандартным действием Realm и отлично работает везде.

updateUI (), по сути, вызывает adapter.notifyDataSetChanged () вызовите, в данном случае, RealmCardViewMediaAdapter.

Если я либо не использую отдельный поток, либо не пытаюсь ограничить количество элементов, возвращаемых адаптером, максимум до 5 элементов, тогда все это прекрасно работает.

Если я оставлю предел 5 в качестве возвращаемого значения из getItemCount () и не буду обновлять sh пользовательский интерфейс, пока все не будет добавлено, то это также работает, даже из другого потока.

Так что кажется, что notifyDataSetChanged () вызывается как список управляемых объектов, основанный на Realm. Обновление в реальном времени, генерирующее эту ошибку. Но я не знаю, почему или как это исправить?

UPDATE END


Я использую Realm Java DB 6.0.2 и realm: android -адаптеры: 3.1 .0

Я создал класс, который расширяет класс RealmRecyclerViewAdapter для моего RecyclerView:

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

Теперь моя проблема - когда я делал эту работу полностью из потока пользовательского интерфейса, у меня не было ошибок. Но теперь, когда транзакция зафиксирована, я получаю:

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 уже зарегистрировал слушателей, все держал прямо и т. Д. c. Что еще мне нужно сделать?

Причина, по которой я использую отдельный поток, заключается в том, что если пользователь идентифицирует несколько десятков (или, возможно, сотен) элементов в списке для удаления, это может занять несколько секунд. выполните удаление (в зависимости от того, о каком списке мы говорим - существуют различные проверки и другие обновления, которые должны произойти с предпочтениями и т. д. c.), и я не хотел блокировать пользовательский интерфейс во время этого процесса.

Как адаптер становится "несовместимым"?

1 Ответ

0 голосов
/ 11 марта 2020

Я решил это, немного подкорректировав архитектуру. Кажется, есть некоторая проблема с StaggeredGridLayoutManager при смешивании с:

  1. Динамическая c способность RealmRecyclerView автоматически обновляться во время массового добавления или удаления.
  2. Адаптер, который пытается ограничить то, что показано, возвращая счетчик из getItemCount (), который не равен текущему счетчику списка.

Я подозреваю, что это связано с тем, как экземпляры ViewHolder создаются и позиционируются менеджером макета, поскольку именно на это указывает ошибка.

Поэтому вместо того, чтобы адаптер возвращал значение, которое может быть меньше, чем фактический счетчик списка, которым управляют в любой момент времени, я вместо этого возвращал значение. изменил запрос к области, чтобы использовать возможность .limit (). Даже когда запрос возвращает изначально меньше лимита, у него есть приятный побочный эффект, заключающийся в ограничении запрошенного лимита, поскольку список динамически растет из массового добавления. И оно имеет то преимущество, что позволяет getItemCount () возвращать любой текущий размер этого списка (который всегда работает).

Подводя итог - в «Представлении месяца» (где я хочу, чтобы пользователь видел только максимум 5 изображений, как на снимке экрана выше), первым шагом является заполнение адаптера верхнего уровня RealmRecyclerView результатом запроса типа DISTINCT, который приводит к OrderedRealmCollection объектов Media, которые соответствуют каждому месяцу каждого года в my media Library.

Затем в потоке «связывания» этого адаптера 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, связанного с этим указанным c месяц, использует результаты этого запроса в качестве своего списка для управления.

Теперь он может возвращать getData (). Size () в результате вызова getItemCount () независимо от того, нахожусь ли я в представлении Month (ограничено лимитом ()) или в представлении моей недели, которое возвращает все элементы мультимедиа за эту неделю и год.

...