ListAdapter не обновляет элемент в RecyclerView

89

Я использую новую библиотеку поддержки ListAdapter. Вот мой код для адаптера

class ArtistsAdapter : ListAdapter<Artist, ArtistsAdapter.ViewHolder>(ArtistsDiff()) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(parent.inflate(R.layout.item_artist))
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        fun bind(artist: Artist) {
            itemView.artistDetails.text = artist.artistAlbums
                    .plus(" Albums")
                    .plus(" \u2022 ")
                    .plus(artist.artistTracks)
                    .plus(" Tracks")
            itemView.artistName.text = artist.artistCover
            itemView.artistCoverImage.loadURL(artist.artistCover)
        }
    }
}

Я обновляю адаптер с помощью

musicViewModel.getAllArtists().observe(this, Observer {
            it?.let {
                artistAdapter.submitList(it)
            }
        })

Мой класс diff

class ArtistsDiff : DiffUtil.ItemCallback<Artist>() {
    override fun areItemsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
        return oldItem?.artistId == newItem?.artistId
    }

    override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
        return oldItem == newItem
    }
}

Что происходит, когда submitList вызывается в первый раз, когда адаптер отображает все элементы, но когда submitList вызывается снова с обновленными свойствами объекта, он не повторно отображает измененное представление.

Он повторно отображает представление, когда я прокручиваю список, который, в свою очередь, вызывает bindView()

Кроме того, я заметил, что вызов adapter.notifyDatasSetChanged()после отправки списка отображает представление с обновленными значениями, но я не хочу вызывать, notifyDataSetChanged()потому что адаптер списка имеет встроенные утилиты diff.

Кто-нибудь может мне здесь помочь?

Виреш Чарантимат
источник
Проблема может быть связана с ArtistsDiffреализацией Artistсамой себя.
Tynn 09
Да, я тоже думаю то же самое, но я не могу точно указать на это
Виреш Чарантимат
Вы можете отлаживать его или добавлять операторы журнала. Также вы можете добавить к вопросу соответствующий код.
Tynn 09
также проверьте этот вопрос, я решил его иначе stackoverflow.com/questions/58232606/…
MisterCat 05

Ответы:

100

Изменить: я понимаю, почему это происходит, это не моя точка зрения. Я хочу сказать, что он должен, по крайней мере, выдать предупреждение или вызвать notifyDataSetChanged()функцию. Потому что, видимо, я вызываю submitList(...)функцию не просто так. Я почти уверен, что люди часами пытаются выяснить, что пошло не так, пока не поймут, что submitList () молча игнорирует вызов.

Это из-за Googleстранной логики. Поэтому, если вы передадите тот же список адаптеру, он даже не вызовет DiffUtil.

public void submitList(final List<T> newList) {
    if (newList == mList) {
        // nothing to do
        return;
    }
....
}

Я действительно не понимаю всего этого, ListAdapterесли он не может обрабатывать изменения в том же списке. Если вы хотите изменить элементы в списке, который вы передаете, ListAdapterи увидеть изменения, вам нужно либо создать глубокую копию списка, либо вам нужно использовать обычный RecyclerViewс вашим собственным DiffUtillклассом.

insa_c
источник
5
Поскольку для выполнения сравнения требуется предыдущее состояние. Конечно, он не сможет справиться с этим, если вы перезапишете предыдущее состояние. O_o
EpicPandaForce
29
Да, но в этот момент есть причина, по которой я звоню submitList, верно? Он должен, по крайней мере, вызвать, notifyDataSetChanged()вместо того, чтобы молча игнорировать вызов. Я почти уверен, что люди часами пытаются понять, что пошло не так, пока не submitList()поймут, что звонок игнорируется.
insa_c
5
Итак, я вернулся к RecyclerView.Adapter<VH>и notifyDataSetChanged(). Жизнь сейчас хороша. Потраченное впустую много часов
Удаядитья Баруа
@insa_c Вы можете добавить к своему счету 3 часа, вот сколько я потратил, пытаясь понять, почему мой список не обновлялся в некоторых крайних случаях ...
Бенкри,
1
notifyDataSetChanged()стоит дорого и полностью лишит смысла реализацию на основе DiffUtil. Вы можете быть осторожны и внимательны при вызове submitListтолько с новыми данными, но на самом деле это просто ловушка производительности.
Дэвид Лю
62

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

Принятый ответ правильный, он предлагает объяснение, но не решение.

Что вы можете сделать, если не используете такие библиотеки:

submitList(null);
submitList(myList);

Другое решение - переопределить submitList (который не вызывает такого быстрого мигания) как таковой:

@Override
public void submitList(final List<Author> list) {
    super.submitList(list != null ? new ArrayList<>(list) : null);
}

Или с кодом Kotlin:

override fun submitList(list: List<CatItem>?) {
    super.submitList(list?.let { ArrayList(it) })
}

Сомнительная логика, но работает отлично. Я предпочитаю второй метод, потому что он не вызывает вызов onBind для каждой строки.

RJFares
источник
4
Это взлом. Просто передайте копию списка. .submitList(new ArrayList(list))
Пол Войташек
2
Я потратил последний час, пытаясь понять, в чем проблема с моей логикой. Такая странная логика.
Джерри Окафор
7
@PaulWoitaschek Это не взлом, здесь используется JAVA :) Он используется для исправления многих проблем в библиотеках, где разработчик «спит». Причина, по которой вы выбрали бы это вместо передачи .submitList (new ArrayList (list)), заключается в том, что вы можете отправлять списки в нескольких местах своего кода. Вы можете каждый раз забывать создавать новый массив, поэтому вы переопределяете.
RJFares
1
@ Po10cio Это странно, главным образом потому, что когда они написали это таким образом, предполагалось, что он будет использоваться только с библиотеками ORM, которые каждый раз предлагают новые списки. Если вы передаете тот же список, но обновленный, вам нужно обойти это, и это будет лучший способ
RJFares
1
Даже при использовании Room я сталкиваюсь с аналогичной проблемой.
Бинк
21

с Kotlin вам просто нужно преобразовать свой список в новый MutableList, например, этот или другой тип списка в соответствии с вашим использованием

.observe(this, Observer {
            adapter.submitList(it?.toMutableList())
        })
Мина Самир
источник
Это странно, но преобразование списка в mutableList у меня работает. Благодарность!
Тхань-Нхон Нгуен,
3
Какого черта это работает? Это работает, но очень любопытно, почему это происходит.
марта, 4
на мой взгляд, ListAdapter не должен иметь дело с вашей ссылкой на список, поэтому с этим? .toMutableList () вы отправляете новый список экземпляров адаптеру. Надеюсь, это достаточно ясно для вас. @ March3April4
Мина Самир
Спасибо. Согласно вашему комментарию, я догадался, что ListAdapter получает свой набор данных как форму List <T>, который может быть изменяемым списком или даже неизменяемым списком. Если я передаю неизменяемый список, сделанные мной изменения блокируются самим набором данных, а не ListAdapter.
марта, 4 апреля,
Я думаю, вы получили это @ March3April4 Также позаботьтесь о механизме, который вы используете с утилитами diff, потому что у него также есть обязанности, которые будут вычислять элементы в списке, должны измениться или нет;)
Мина Самир
9

У меня была аналогичная проблема, но неправильный рендеринг был вызван комбинацией setHasFixedSize(true)и android:layout_height="wrap_content". Впервые адаптер поставлялся с пустым списком, поэтому высота никогда не обновлялась 0. Во всяком случае, это решило мою проблему. У кого-то может быть такая же проблема, и он подумает, что проблема в адаптере.

Ян Веселы
источник
1
Да, установите recycleview на wrap_content, чтобы обновить список, если вы установите его на match_parent, он не будет вызывать адаптер
Exel Staderlin
5

Если у вас возникнут проблемы при использовании

recycler_view.setHasFixedSize(true)

вы должны обязательно проверить этот комментарий: https://github.com/oughttbot/expandable-recycler-view/issues/53#issuecomment-362991531

Это решило проблему с моей стороны.

(Вот скриншот комментария по запросу)

введите описание изображения здесь

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

Сегодня тоже наткнулся на эту "проблему". С помощью ответа insa_c в и решения RJFares игровая я сделал себе функцию расширения Котлин:

/**
 * Update the [RecyclerView]'s [ListAdapter] with the provided list of items.
 *
 * Originally, [ListAdapter] will not update the view if the provided list is the same as
 * currently loaded one. This is by design as otherwise the provided DiffUtil.ItemCallback<T>
 * could never work - the [ListAdapter] must have the previous list if items to compare new
 * ones to using provided diff callback.
 * However, it's very convenient to call [ListAdapter.submitList] with the same list and expect
 * the view to be updated. This extension function handles this case by making a copy of the
 * list if the provided list is the same instance as currently loaded one.
 *
 * For more info see 'RJFares' and 'insa_c' answers on
 * /programming/49726385/listadapter-not-updating-item-in-reyclerview
 */
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.updateList(list: List<T>?) {
    // ListAdapter<>.submitList() contains (stripped):
    //  if (newList == mList) {
    //      // nothing to do
    //      return;
    //  }
    this.submitList(if (list == this.currentList) list.toList() else list)
}

который затем можно использовать где угодно, например:

viewModel.foundDevices.observe(this, Observer {
    binding.recyclerViewDevices.adapter.updateList(it)
})

и он только (и всегда) копирует список, если он такой же, как загруженный в данный момент.

Боян П.
источник
3

Согласно официальным документам :

Каждый раз, когда вы вызываете submitList, он отправляет новый список для сравнения и отображения.

Вот почему всякий раз, когда вы вызываете submitList в предыдущем (уже представленном списке), он не вычисляет Diff и не уведомляет адаптер об изменении в наборе данных.

Ашу Тяги
источник
2

Для меня эта проблема возникала, если я использовал RecyclerViewвнутреннюю часть ScrollViewс nestedScrollingEnabled="false"установкой высоты RV wrap_content.
Адаптер обновился правильно, и была вызвана функция привязки, но элементы не были показаны - RecyclerViewфайл застрял в исходном размере.

Переход ScrollViewна NestedScrollViewустраненную проблему.

Томислав
источник
2

В моем случае я забыл установить LayoutManagerдля RecyclerView. Эффект от этого такой же, как описано выше.

just_user
источник
1

Для всех, у кого сценарий такой же, как у меня, я оставляю свое решение, которое я не знаю, почему оно работает, здесь.

Решение, которое сработало для меня, было от @Mina Samir, который отправляет список как изменяемый список.

Мой сценарий проблемы:

-Загрузка списка друзей внутри фрагмента.

  1. ActivityMain прикрепляет FragmentFriendList (наблюдает за живыми данными элементов базы данных друга) и в то же время запрашивает http-запрос на сервер, чтобы получить весь мой список друзей.

  2. Обновите или вставьте элементы с http-сервера.

  3. Каждое изменение запускает обратный вызов onChanged для живых данных. Но когда я впервые запускаю приложение, а это значит, что на моем столе ничего не было, submitList завершается успешно без каких-либо ошибок, но на экране ничего не появляется.

  4. Однако когда я запускаю приложение во второй раз, данные загружаются на экран.

Решение, как было сказано выше, заключается в отправке списка как mutableList.

3 марта, 4 апреля
источник
1

У меня была аналогичная проблема. Проблема была в Diffфункциях, которые неадекватно сравнивали элементы. Всем, у кого возникла эта проблема, убедитесь, что ваши Diffфункции (и, как следствие, классы объектов данных) содержат правильные определения для сравнения, т.е. сравнение всех полей, которые могут быть обновлены в новом элементе. Например в исходном посте

    override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
    return oldItem == newItem
}

Эта функция (потенциально) не делает то, что написано на этикетке: она не сравнивает содержимое двух элементов, если только вы не переопределили equals()функцию в Artistклассе. В моем случае у меня этого не было, и определение areContentsTheSameтолько проверило одно из необходимых полей из-за моего недосмотра при его реализации. Это структурное равенство против ссылочного равенства, вы можете найти больше об этом здесь

ампалмер
источник
0

Мне нужно было изменить мой DiffUtils

override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {

Чтобы на самом деле вернуть, является ли содержимое новым, а не просто сравнить идентификатор модели.

тонизирующие средства
источник
0

Использование первого ответа @RJFares успешно обновляет список, но не поддерживает состояние прокрутки. Все RecyclerViewначинается с 0-й позиции. В качестве обходного пути я сделал следующее:

   fun updateDataList(newList:List<String>){ //new list from DB or Network

     val tempList = dataList.toMutableList() // dataList is the old list
     tempList.addAll(newList)
     listAdapter.submitList(tempList) // Recyclerview Adapter Instance
     dataList = tempList

   }

Таким образом, я могу поддерживать состояние прокрутки RecyclerViewвместе с измененными данными.

iCantC
источник