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

Мій диф клас

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()оскільки в адаптер списку вбудовано різну використання

Хтось може мені тут допомогти?


Проблема може бути пов’язана, ArtistsDiffа отже, із Artistсамою реалізацією .
tynn

Так, я теж думаю те саме, але я, здається, не можу це
визначити

Ви можете налагодити його або додати оператор журналу. Також ви можете додати відповідний код до запитання.
tynn

також перевірити це питання, я вирішив інакше stackoverflow.com/questions/58232606 / ...
MisterCat

Відповіді:


100

Редагувати: Я розумію, чому так трапляється, що не було моєю метою. Моя думка полягає в тому, що принаймні потрібно дати попередження або викликати notifyDataSetChanged()функцію. Тому що, мабуть, я викликаю submitList(...)функцію з причини. Я майже впевнений, що люди намагаються з'ясувати, що пішло не так годинами, поки не зрозуміють, що submitList () мовчки ігнорує виклик.

Це через Googleдивну логіку. Отже, якщо ви передаєте той самий список адаптеру, він навіть не викликає DiffUtil.

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

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


5
Оскільки для виконання різниці потрібен попередній стан. Звичайно, він не справляється з цим, якщо ви перезаписали попередній стан. O_o
EpicPandaForce

30
Так, але на той момент є причина, чому я телефоную submitList, так? Він повинен принаймні зателефонувати notifyDataSetChanged()замість того, щоб мовчки ігнорувати дзвінок. Я майже впевнений, що люди намагаються зрозуміти, що пішло не так годинами, поки submitList()мовчки ігнорують дзвінок.
insa_c

6
Тож я повернувся до RecyclerView.Adapter<VH>та notifyDataSetChanged(). Життя зараз добре.
Даремно витрачена

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

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

Або з кодом Котліна:

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

Сумнівна логіка, але працює чудово. Моїм переважним методом є другий, оскільки він не призводить до кожного рядка, щоб отримати виклик onBind.


4
Це хак. Просто передайте копію списку. .submitList(new ArrayList(list))
Пол Войташек,

2
Я витратив останню годину, намагаючись з’ясувати, в чому проблема з моєю логікою. Така дивна логіка.
Джеррі Окафор,

7
@PaulWoitaschek Це не хакерство, тут використовується JAVA :), воно використовується для виправлення багатьох проблем у бібліотеках, де розробник "спить". Причина, чому ви вибрали це замість того, щоб передати .submitList (новий ArrayList (список)), полягає в тому, що ви можете подавати списки в декількох місцях свого коду. Ви можете забути створювати новий масив кожного разу, тому ви перевизначаєте.
RJFares

1
@ Po10cio Це дивно в основному тому, що коли вони писали це так, передбачалося, що воно буде використовуватися лише з бібліотеками ORM, які пропонують нові списки кожного разу. Якщо ви проходите той самий список, але оновлений, ви повинні обійти це, і це був би найкращий спосіб
RJFares

1
Навіть використовуючи Room, я стикаюся з подібною проблемою.
Bink

21

з Kotlin вам просто потрібно перетворити свій список на новий MutableList, такий як той чи інший тип списку, відповідно до вашого використання

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

Це дивно, але перетворення списку на mutableList мені підходить. Дякую!
Тхань-Нхон Нгуєн

3
Чому, блін, це працює? Це працює, але дуже цікаво, чому це трапляється.
березня, 4

на мою думку, ListAdapter не повинен стосуватися посилання на ваш список, отже, з цим? .toMutableList () ви подаєте новий список екземплярів на адаптер. Сподіваюся, для вас це досить ясно. @ March3April4
Міна Самір

Дякую. Згідно з вашим коментарем, я здогадався, що ListAdapter отримує свій набір даних у вигляді List <T>, який може бути змінним списком або навіть незмінним списком. Якщо я передаю незмінний список, внесені мною зміни блокуються самим набором даних, а не ListAdapter.
березня, 4

Я думаю, ви зрозуміли це @ March3April4 Крім того, дбайте про механізм, який ви використовуєте з різницею використання, оскільки він також несе відповідальність, розраховуватиме, що елементи в списку повинні змінюватися чи ні;)
Міна Самір,

10

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


1
Так, встановіть recycleview на wrap_content оновить список, якщо ви встановите його на match_parent, він не буде викликати адаптер
Exel Staderlin

5

Якщо у вас виникають деякі проблеми під час використання

recycler_view.setHasFixedSize(true)

вам слід обов'язково перевірити цей коментар: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531

Це вирішило проблему на моєму боці.

(Ось скріншот коментаря за запитом)

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


Посилання на рішення вітається, але будь ласка, переконайтеся, що ваша відповідь корисна без нього: додайте контекст навколо посилання, щоб ваші однодумці мали певне уявлення, що це таке і чому воно є, а потім цитуйте найвідповіднішу частину сторінки, яку ви хочете повторне посилання на випадок, якщо цільова сторінка буде недоступна.
Мостафа Аріан Неджад,

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. Ефект від цього такий же, як описано вище.


1

Для тих, хто має такий самий сценарій, як я, я залишаю своє рішення, яке я не знаю, чому воно працює, тут.

Рішення, яке спрацювало для мене, було від @Mina Samir, яка подає список як змінний список.

Сценарій мого випуску:

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

  1. ActivityMain додає FragmentFriendList (Спостерігає за даними live db елементів друга) і одночасно запитує на сервер запит http, щоб отримати весь мій список друзів.

  2. Оновіть або вставте елементи з сервера http.

  3. Кожна зміна запалює зворотний виклик onChanged даних live. Але коли я вперше запускаю програму, а це означає, що на моєму столі нічого не було, submitList вдається без будь-якої помилки, але на екрані нічого не відображається.

  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 first answer успішно оновлює список, але не підтримує стан прокрутки. Весь 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разом із модифікованими даними.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.