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

Я использую новую библиотеку поддержки 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.04.2018 10:05

Да, я тоже думаю то же самое, но я не могу точно указать на это

Veeresh Charantimath 09.04.2018 10:07

Вы можете отлаживать его или добавлять операторы журнала. Также вы можете добавить к вопросу соответствующий код.

tynn 09.04.2018 11:03

также проверьте этот вопрос, я решил по-другому stackoverflow.com/questions/58232606/…

MisterCat 05.10.2019 07:16

DiffUtil использует разностный алгоритм Юджина В. Майерса для вычисления минимального количества обновлений для преобразования одного списка в другой. Короче говоря, этот алгоритм работает в двух разных списках. DiffUtil используется AsyncListDiffer, который запускает алгоритм в фоновом потоке и обновляет представление ресайклера в основном потоке. AsyncListDiffer поддерживает список как предыдущий контейнер списка, из-за чего мы должны предоставить новый экземпляр списка, если мы хотим оптимальной производительности Diffutils.

Mukul Pathak 24.02.2021 07:07
116
5
32 699
17
Перейти к ответу Данный вопрос помечен как решенный

Ответы 17

Ответ принят как подходящий

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

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

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

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

Поскольку для выполнения сравнения требуется предыдущее состояние. Конечно, он не справится, если вы перезапишете предыдущее состояние. О_о

EpicPandaForce 27.04.2018 14:26

Да, но в этот момент есть причина, по которой я называю submitList, верно? Он должен, по крайней мере, вызвать notifyDataSetChanged() вместо того, чтобы молча игнорировать вызов. Я почти уверен, что люди часами пытаются выяснить, что пошло не так, пока не поймут, что submitList() молча игнорирует звонок.

insa_c 27.04.2018 17:25

Итак, я вернулся к RecyclerView.Adapter<VH> и notifyDataSetChanged(). Жизнь сейчас хороша. Потраченное впустую много часов

Udayaditya Barua 19.03.2020 12:40

@insa_c Вы можете добавить 3 часа к своему счету, вот сколько я потратил зря, пытаясь понять, почему мой список не обновлялся в некоторых крайних случаях ...

Bencri 23.06.2020 17:14
notifyDataSetChanged() стоит дорого и полностью лишит смысла реализацию на основе DiffUtil. Вы можете быть осторожны и осознанны, вызывая submitList только с новыми данными, но на самом деле это просто ловушка производительности.
David Liu 08.09.2020 06:20

Я думал, что что-то упустил или в моем коде есть ошибка. это было плохо :( спасибо за ваш ответ.

Reza 27.01.2021 19:04

Вот почему изменяемый список - зло. Отправить список, изменить его после и отправить тот же экземпляр? ай! Зачем снова подавать тот же список? Если вы изменили список незаметно, просто вызовите notifyDataSetChanged(), чтобы проинформировать адаптер, нет смысла передавать ту же ссылку снова.

RadekJ 26.05.2021 09:03

Библиотека предполагает, что вы используете 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 для каждой строки.

Это точно правильно, но в моем ответе также предлагается ваше второе решение :)

insa_c 27.04.2018 17:27

Я просто привел конкретный пример для всех, кому интересно, как ее решить. Один пример для быстрого исправления и один для переопределения логики в библиотеке. Вы указали причину проблемы, но, предполагая, что не все хорошо разбираются в программировании, я просто предложил им решение, а также почему это было написано таким образом Google :)

RJFares 29.04.2018 10:04

Это взлом. Просто передайте копию списка. .submitList(new ArrayList(list))

Paul Woitaschek 21.08.2018 15:48

Я потратил последний час, пытаясь понять, в чем проблема с моей логикой. Такая странная логика.

Jerry Okafor 10.10.2018 15:04

@PaulWoitaschek Это не взлом, это использует JAVA :) Он используется для исправления многих проблем в библиотеках, где разработчик «спит». Причина, по которой вы выбрали бы это вместо передачи .submitList (new ArrayList (list)), заключается в том, что вы можете отправлять списки в нескольких местах вашего кода. Вы можете каждый раз забывать создавать новый массив, поэтому вы переопределяете.

RJFares 10.10.2018 15:20

@ Po10cio Это странно главным образом потому, что когда они написали это таким образом, предполагалось, что он будет использоваться только с библиотеками ORM, которые каждый раз предлагают новые списки. Если вы передаете тот же список, но обновленный, вам нужно обойти это, и это будет лучший способ

RJFares 10.10.2018 15:24

Даже при использовании Room я сталкиваюсь с аналогичной проблемой.

Bink 20.12.2018 00:59

По-видимому, это работает при обновлении списка в модели просмотра новыми элементами, но когда я обновляю свойство (логическое - isSelected) элемента в списке, это все равно не работает .. Идентификатор почему, но DiffUtil возвращает тот же старый и новый элемент, что и я ' я проверил. Есть идеи, где может возникнуть проблема?

Ralph 10.04.2019 05:01

@Ralph убедитесь, что вы переопределили equals () и hashcode () в своих моделях, и убедитесь, что свойство (isSelected) находится в этих методах

RJFares 11.04.2019 14:11

@RJFares благодарим вас за ответ, но он все еще не работает в моем текущем проекте. Попробую создать образец приложения и снова протестировать

Ralph 12.04.2019 04:52

Мои списки каждый раз разные, и areItemsTheSame на самом деле вызывается. Однако, когда areItemsTheSame возвращает false, я ожидаю, что произойдет привязка к держателю просмотра. Несмотря на то, что он возвращает false для всех элементов, кроме одного, onBindViewHolder вызывается только для одного элемента, что не имеет смысла. В результате мой recyclerview обновляет только один элемент. Использование вашего решения решает проблему. Это действительно похоже на ошибку в DiffUtils.

AndroidDev 14.12.2019 12:58

@Ralph У меня точно такой же сценарий, когда в новом списке я изменяю только свойство isSelected, а DiffUtil не работает, даже с переопределением equals () и hash (). Вы нашли решение?

Cosmin Vacaru 10.09.2020 15:26

@PaulWoitaschek, это не сработает. Потому что это мелкая копия, а не глубокая копия.

user175257 26.02.2021 08:53

@CosminVacaru Я знаю, что это поздно, но это потому, что адаптер ссылается на тот же список, элементы которого вы изменяете.

Tayyab Mazhar 13.04.2021 14:00

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

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

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

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

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

Exel Staderlin 17.06.2020 19:33

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

/**
 * 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
 * https://stackoverflow.com/questions/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)
})

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

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

recycler_view.setHasFixedSize(true)

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

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

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

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

Mostafa Arian Nejad 03.09.2019 20:51

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

.observe(this, Observer {
            adapter.submitList(it?.toMutableList())
        })

Это странно, но преобразование списка в mutableList у меня работает. Спасибо!

Thanh-Nhon Nguyen 26.03.2020 11:25

Какого черта это работает? Это работает, но очень любопытно, почему это происходит.

March3April4 21.04.2020 09:45

на мой взгляд, ListAdapter не должен иметь дело со ссылкой на ваш список, поэтому с его помощью? .toMutableList () вы отправляете новый список экземпляров адаптеру. Надеюсь, это достаточно ясно для вас. @ March3April4

Mina Samir 16.05.2020 03:38

Спасибо. Согласно вашему комментарию, я догадался, что ListAdapter получает свой набор данных в виде формы List <T>, которая может быть изменяемым списком или даже неизменным списком. Если я передаю неизменяемый список, сделанные мной изменения блокируются самим набором данных, а не ListAdapter.

March3April4 19.05.2020 08:33

Я думаю, вы получили это @ March3April4 Кроме того, позаботьтесь о механизме, который вы используете с утилитами diff, потому что у него также есть обязанности по вычислению элементов в списке, должны измениться или нет;)

Mina Samir 19.05.2020 11:20

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

Замена ScrollView на NestedScrollView устранила проблему.

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

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

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

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

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

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

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

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

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

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

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

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

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

Использование первого ответа @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 вместе с измененными данными.

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

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

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

Причина, по которой ваш ListAdapter .submitlist не вызывается, заключается в том, что объект вы обновили по-прежнему сохраняет тот же адрес в памяти.

Когда вы обновляете объект, скажем, .setText, он изменяет значение в исходном объекте.

Так что, когда вы проверите, если object.id == object2.id, он вернется как тот же потому что оба имеют ссылку на одно и то же место в памяти.

Решение состоит в том, чтобы создать новый объект с обновленными данными и вставить его в свой список. Затем будет вызван submitList, и он будет работать правильно

Потратил столько времени на то, чтобы разобраться в проблеме в том же случае.

Но в моей ситуации проблема заключалась в том, что я забыл указать layoutManager для моего recyclerView: vRecyclerView.layoutManager = LinearLayoutManager(requireContext())

Надеюсь, никто не повторит мою ошибку ...

app: layoutManager = "androidx.recyclerview.widget.LinearLayout‌ Manager" теперь стал намного лучше :)

Aetherna 05.02.2021 21:17

Просто хочу проголосовать за это, потому что я совершил ту же глупую ошибку.

pqtuan86 14.04.2021 10:32

Оптимальное решение: для Котлина

        var list :ArrayList<BaseModel> = ArrayList(adapter.currentList)
        list.add(Item("Content"))
        adapter.submitList(list) {
            Log.e("ListAdaptor","List Updated Successfully")
        }

Мы не должны поддерживать другой базовый список, так как adapter.currentList вернет список, в котором уже вычислен diff.

Мы должны предоставлять новый экземпляр каждый раз, когда список обновляется из-за DiffUtil. Согласно документации Android DiffUtil - это служебный класс, который вычисляет разницу между двумя списками и выводит список операций обновления, которые преобразуют первый список во второй. Один список уже поддерживается AsyncListDiffer, который запускает diffutil в фоновом потоке, а другой должен быть передан с помощью adaptor.submitList ()

Это решает мою проблему. Я думаю, что лучший способ - не переопределять шину submitList, добавляя новую функцию для добавления нового списка.

    fun updateList(list: MutableList<ScaleDispBlock>?) {
        list?.let {
             val newList = ArrayList<ScaleDispBlock>(list)
            submitList(newList)
        }
    }

Другие вопросы по теме