ListAdapter nie aktualizuje elementu w RecyclerView

89

Używam nowej biblioteki pomocy ListAdapter. Oto mój kod adaptera

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

Aktualizuję adapter za pomocą

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

Moja klasa różnicowa

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

Dzieje się tak, gdy funkcja submitList jest wywoływana przy pierwszym renderowaniu przez adapter wszystkich elementów, ale ponowne wywołanie metody submitList ze zaktualizowanymi właściwościami obiektu nie powoduje ponownego renderowania zmienionego widoku.

Ponownie renderuje widok, gdy przewijam listę, która z kolei wywołuje bindView()

Zauważyłem również, że wywołanie adapter.notifyDatasSetChanged()po przesłaniu listy renderuje widok ze zaktualizowanymi wartościami, ale nie chcę wywoływać, notifyDataSetChanged()ponieważ adapter listy ma wbudowane narzędzia różnicowe

Czy ktoś mógłby mi tu pomóc?

Veeresh Charantimath
źródło
Problem może być związany ArtistsDiffz realizacją Artistsamego siebie.
tynn
Tak, ja też myślę tak samo, ale nie mogę tego wskazać
Veeresh Charantimath
Możesz go debugować lub dodać instrukcje dziennika. Możesz również dodać odpowiedni kod do pytania.
tynn
też sprawdź to pytanie, rozwiązałem to inaczej stackoverflow.com/questions/58232606/ ...
MisterCat

Odpowiedzi:

100

Edycja: Rozumiem, dlaczego tak się dzieje, nie o to mi chodziło. Chodzi mi o to, że przynajmniej musi dać ostrzeżenie lub wywołać notifyDataSetChanged()funkcję. Ponieważ najwyraźniej wywołuję tę submitList(...)funkcję z jakiegoś powodu. Jestem prawie pewien, że ludzie godzinami próbują dowiedzieć się, co poszło nie tak, dopóki nie zorientują się, że submitList () po cichu ignoruje wywołanie.

To z powodu Googledziwnej logiki. Więc jeśli przekażesz tę samą listę do adaptera, to nawet nie wywoła DiffUtil.

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

Naprawdę nie rozumiem całego tego sensu, ListAdapterjeśli nie obsługuje zmian na tej samej liście. Jeśli chcesz zmienić elementy na liście, którą przekazujesz, ListAdapteri zobaczyć zmiany, albo musisz utworzyć głęboką kopię listy, albo musisz używać zwykłego RecyclerViewz własną DiffUtillklasą.

insa_c
źródło
5
Ponieważ wymaga poprzedniego stanu, aby wykonać różnicę. Oczywiście nie poradzi sobie z tym, jeśli nadpiszesz poprzedni stan. O_o
EpicPandaForce
29
Tak, ale w tym momencie jest powód, dla którego dzwonię submitList, prawda? Powinien przynajmniej wywołać połączenie notifyDataSetChanged()zamiast po cichu ignorować połączenie. Jestem prawie pewien, że ludzie godzinami próbują dowiedzieć się, co poszło nie tak, dopóki nie zorientują się, że submitList()po cichu ignoruje telefon.
insa_c
5
Więc wróciłem do RecyclerView.Adapter<VH>i notifyDataSetChanged(). Życie jest teraz dobre. Zmarnowałem sporo godzin
Udayaditya Barua
@insa_c Możesz dodać 3 godziny do swojej liczby, tyle straciłem, próbując zrozumieć, dlaczego mój widok listy nie aktualizował się w niektórych
skrajnych
notifyDataSetChanged()jest drogi i całkowicie pokonałby sens posiadania implementacji opartej na DiffUtil. Możesz być ostrożny i uważny, dzwoniąc submitListtylko z nowymi danymi, ale tak naprawdę to tylko pułapka wydajności.
David Liu
62

Biblioteka zakłada, że ​​używasz Room lub innego ORM, który oferuje nową listę asynchroniczną za każdym razem, gdy jest aktualizowana, więc samo wywołanie submitList na niej zadziała, a dla niechlujnych programistów zapobiega dwukrotnemu wykonywaniu obliczeń, jeśli zostanie wywołana ta sama lista.

Przyjęta odpowiedź jest prawidłowa, zawiera wyjaśnienie, ale nie zawiera rozwiązania.

Jeśli nie używasz takich bibliotek, możesz:

submitList(null);
submitList(myList);

Innym rozwiązaniem byłoby zastąpienie submitList (co nie powoduje tak szybkiego migania) jako takiego:

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

Lub z kodem Kotlin:

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

Wątpliwa logika, ale działa doskonale. Moją preferowaną metodą jest druga, ponieważ nie powoduje ona, że ​​każdy wiersz otrzymuje wywołanie onBind.

RJFares
źródło
4
To hack. Po prostu przekaż kopię listy. .submitList(new ArrayList(list))
Paul Woitaschek
2
Spędziłem ostatnią godzinę próbując dowiedzieć się, na czym polega problem z moją logiką. Taka dziwna logika.
Jerry Oka,
7
@PaulWoitaschek To nie jest hack, to używa JAVA :) Służy do naprawiania wielu problemów w bibliotekach, w których programista „śpi”. Powodem, dla którego wybierasz tę opcję zamiast przekazywania .submitList (new ArrayList (list)) jest to, że możesz przesyłać listy w wielu miejscach w kodzie. Możesz zapomnieć o utworzeniu nowej tablicy za każdym razem, dlatego nadpisujesz.
RJFares
1
@ Po10cio To dziwne, głównie dlatego, że kiedy napisali to w ten sposób, założono, że będzie używany tylko z bibliotekami ORM, które za każdym razem oferują nowe listy. Jeśli przekazujesz tę samą listę, ale zaktualizowaną, musisz to obejść, a to byłby najlepszy sposób
RJFares
1
Nawet używając Room mam podobny problem.
Bink
20

z Kotlinem wystarczy przekonwertować swoją listę na nową MutableList, taką jak ta lub inny typ listy zgodnie z Twoim użyciem

.observe(this, Observer {
            adapter.submitList(it?.toMutableList())
        })
Mina Samir
źródło
To dziwne, ale konwersja listy na mutableList działa dla mnie. Dzięki!
Thanh-Nhon Nguyen
3
Dlaczego do diabła to działa? Działa, ale jest bardzo ciekawy, dlaczego tak się dzieje.
marca 4
moim zdaniem ListAdapter nie może zajmować się twoim odwołaniem do listy, więc witając? .toMutableList () przesyłasz nową listę instancji do adaptera. Mam nadzieję, że to dla ciebie wystarczająco jasne. @ Marca3 kwietnia4
Mina Samir
Dzięki. Zgodnie z twoim komentarzem zgadłem, że ListAdapter odbiera swój zestaw danych jako formę List <T>, która może być listą zmienną lub nawet listą niezmienną. Jeśli przekażę niezmienną listę, wprowadzone zmiany są blokowane przez sam zestaw danych, a nie przez ListAdapter.
marca 4
Myślę, że dostałeś to @ March3April4. Również, dbaj o mechanizm, którego używasz z narzędziami diff, ponieważ ma on również obowiązki, obliczy pozycje na liście, czy powinny się zmienić;)
Mina Samir
9

Miałem podobny problem, ale nieprawidłowe renderowanie było spowodowane kombinacją setHasFixedSize(true)i android:layout_height="wrap_content". Po raz pierwszy adapter został dostarczony z pustą listą, więc wysokość nigdy nie była aktualizowana i była 0. W każdym razie to rozwiązało mój problem. Ktoś inny może mieć ten sam problem i pomyśli, że problem dotyczy adaptera.

Jan Veselý
źródło
1
Tak, ustawienie recyclingleview na wrap_content zaktualizuje listę, jeśli ustawisz ją na match_parent, nie wywoła adaptera
Exel Staderlin
5

Jeśli napotkasz problemy podczas używania

recycler_view.setHasFixedSize(true)

zdecydowanie powinieneś sprawdzić ten komentarz: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531

To rozwiązało problem po mojej stronie.

(Oto zrzut ekranu z żądanym komentarzem)

wprowadź opis obrazu tutaj

Yoann.G
źródło
Link do rozwiązania jest mile widziany, ale upewnij się, że Twoja odpowiedź jest przydatna bez niego: dodaj kontekst wokół linku, aby inni użytkownicy mieli pojęcie, co to jest i dlaczego się tam znajduje, a następnie zacytuj najbardziej odpowiednią część strony, którą podałeś. ponowne łącze w przypadku, gdy strona docelowa jest niedostępna.
Mostafa Arian Nejad
4

Dziś też natknąłem się na ten „problem”. Z pomocą odpowiedź insa_c za i rozwiązania RJFares koszulka Zrobiłem sobie funkcję przedłużacza 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
 * /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)
}

które można następnie wykorzystać w dowolnym miejscu, np .:

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

i tylko (i zawsze) kopiuje listę, jeśli jest taka sama jak aktualnie załadowana.

Bojan P.
źródło
3

Według oficjalnych dokumentów :

Za każdym razem, gdy wywołujesz submitList , przesyła nową listę do porównania i wyświetlenia.

Dlatego za każdym razem, gdy wywołujesz submitList na poprzedniej (już przesłanej liście), nie oblicza Diff i nie powiadamia adaptera o zmianie w zestawie danych.

Ashu Tyagi
źródło
2

U mnie ten problem pojawił się, gdy korzystałem z RecyclerViewwnętrza ScrollViewz nestedScrollingEnabled="false"i wysokości kampera ustawionej na wrap_content.
Adapter został poprawnie zaktualizowany i wywołano funkcję bind, ale elementy nie zostały wyświetlone - karta RecyclerViewutknęła w swoim oryginalnym rozmiarze.

Zmiana, ScrollViewaby NestedScrollViewrozwiązać problem.

Tomislav
źródło
2

W moim przypadku zapomniałem ustawić LayoutManagerdla RecyclerView. Efekt jest taki sam, jak opisano powyżej.

just_user
źródło
1

Dla każdego, kto ma taki sam scenariusz jak mój, zostawiam tutaj swoje rozwiązanie, którego nie wiem, dlaczego działa.

Rozwiązaniem, które zadziałało, było dla mnie @Mina Samir, która przesyła listę jako zmienną listę.

Mój scenariusz problemu:

-Ładowanie listy znajomych wewnątrz fragmentu.

  1. ActivityMain dołącza FragmentFriendList (obserwuje dane z listy znajomych w bazie danych) i jednocześnie wysyła żądanie http do serwera, aby pobrać całą moją listę znajomych.

  2. Zaktualizuj lub wstaw elementy z serwera http.

  3. Każda zmiana zapala wywołanie zwrotne onChanged liveata. Ale kiedy uruchamiam aplikację po raz pierwszy, co oznacza, że ​​na moim stole nic nie było, lista submitList powiodła się bez żadnego błędu, ale nic nie pojawia się na ekranie.

  4. Jednak gdy uruchamiam aplikację po raz drugi, dane są ładowane na ekran.

Rozwiązaniem jest, jak wspomniano powyżej, przesłanie listy jako mutableList.

3 marca 4 kwietnia 4
źródło
1

Miałem podobny problem. Problem dotyczył Difffunkcji, które nie porównywały odpowiednio elementów. Każdy, kto ma ten problem, upewnij się, że twoje Difffunkcje (a co za tym idzie, klasy obiektów danych) zawierają właściwe definicje porównawcze - tj. Porównując wszystkie pola, które mogą zostać zaktualizowane w nowej pozycji. Na przykład w oryginalnym poście

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

Ta funkcja (potencjalnie) nie robi tego, co jest napisane na etykiecie: nie porównuje zawartości dwóch elementów - chyba że nadpisałeś equals()funkcję w Artistklasie. W moim przypadku tego nie zrobiłem, a definicję areContentsTheSamesprawdziłem tylko w jednym z niezbędnych pól, ze względu na mój niedopatrzenie przy jego wdrażaniu. To jest równość strukturalna kontra równość referencyjna, więcej na ten temat można znaleźć tutaj

ampalmer
źródło
0

Musiałem zmodyfikować moje DiffUtils

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

Aby faktycznie zwrócić, czy zawartość jest nowa, a nie tylko porównać identyfikator modelu.

środki tonizujące
źródło
0

Użycie @RJFares pierwszej odpowiedzi aktualizuje listę pomyślnie, ale nie utrzymuje stanu przewijania. Całość RecyclerViewzaczyna się od 0 pozycji. Aby obejść ten problem, zrobiłem to:

   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

   }

W ten sposób jestem w stanie utrzymać stan przewijania RecyclerViewwraz ze zmodyfikowanymi danymi.

iCantC
źródło