ListAdapter aktualisiert Element in RecyclerView nicht

89

Ich verwende die neue Support-Bibliothek ListAdapter. Hier ist mein Code für den Adapter

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

Ich aktualisiere den Adapter mit

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

Meine Diff-Klasse

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

Was passiert, ist, wenn submitList zum ersten Mal aufgerufen wird, wenn der Adapter alle Elemente rendert, aber wenn submitList mit aktualisierten Objekteigenschaften erneut aufgerufen wird, wird die geänderte Ansicht nicht erneut gerendert.

Es rendert die Ansicht neu, während ich durch die Liste scrolle, was wiederum aufruft bindView()

Außerdem ist mir aufgefallen, dass beim Aufruf adapter.notifyDatasSetChanged()nach dem Senden der Liste die Ansicht mit aktualisierten Werten gerendert wird. Ich möchte jedoch nicht aufrufen, notifyDataSetChanged()da im Listenadapter verschiedene Dienstprogramme integriert sind

Kann mir hier jemand helfen?

Veeresh Charantimath
quelle
Das Problem könnte mit ArtistsDiffund damit mit der Implementierung von sich Artistselbst zusammenhängen.
tynn
Ja, ich denke auch das gleiche, aber ich kann es nicht
genau sagen
Sie können es debuggen oder Protokollanweisungen hinzufügen. Sie können der Frage auch den entsprechenden Code hinzufügen.
tynn
Überprüfen Sie auch diese Frage, ich habe es anders gelöst stackoverflow.com/questions/58232606/…
MisterCat

Antworten:

100

Edit: Ich verstehe, warum das passiert, das war nicht mein Punkt. Mein Punkt ist, dass es zumindest eine Warnung geben oder die notifyDataSetChanged()Funktion aufrufen muss . Weil ich die submitList(...)Funktion anscheinend aus einem bestimmten Grund aufrufe. Ich bin mir ziemlich sicher, dass die Leute versuchen herauszufinden, was stundenlang schief gelaufen ist, bis sie herausgefunden haben, dass submitList () den Aufruf stillschweigend ignoriert.

Dies liegt an der Googleseltsamen Logik. Wenn Sie also dieselbe Liste an den Adapter übergeben, wird nicht einmal der aufgerufen DiffUtil.

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

Ich verstehe den ganzen Punkt wirklich nicht, ListAdapterwenn es nicht möglich ist, Änderungen an derselben Liste zu verarbeiten. Wenn Sie die Elemente in der Liste ändern möchten, die Sie an übergeben, ListAdapterund die Änderungen anzeigen möchten, müssen Sie entweder eine tiefe Kopie der Liste erstellen oder sie regelmäßig RecyclerViewmit Ihrer eigenen DiffUtillKlasse verwenden.

insa_c
quelle
5
Weil es den vorherigen Zustand erfordert, um das Diff auszuführen. Natürlich kann es nicht damit umgehen, wenn Sie den vorherigen Status überschreiben. O_o
EpicPandaForce
30
Ja, aber an diesem Punkt gibt es einen Grund, warum ich das anrufe submitList, richtig? Es sollte zumindest den Anruf anrufen, notifyDataSetChanged()anstatt den Anruf stillschweigend zu ignorieren. Ich bin mir ziemlich sicher, dass die Leute versuchen herauszufinden, was stundenlang schief gelaufen ist, bis sie herausfinden, dass submitList()der Anruf stillschweigend ignoriert wird.
insa_c
6
Also bin ich zurück zu RecyclerView.Adapter<VH>und notifyDataSetChanged(). Das Leben ist jetzt gut. Verschwendete eine gute Anzahl von Stunden
Udayaditya Barua
@insa_c Sie können Ihrer Zählung 3 Stunden hinzufügen. So viel habe ich damit verschwendet, zu verstehen, warum meine Listenansicht in einigen Randfällen nicht aktualisiert wurde ...
Bencri
1
notifyDataSetChanged()ist teuer und würde den Punkt einer DiffUtil-basierten Implementierung völlig zunichte machen. Sie können vorsichtig und absichtlich sein, wenn Sie submitListnur mit neuen Daten anrufen , aber das ist wirklich nur eine Leistungsfalle.
David Liu
62

Die Bibliothek geht davon aus, dass Sie Room oder ein anderes ORM verwenden, das bei jeder Aktualisierung eine neue asynchrone Liste bietet. Wenn Sie also nur submitList aufrufen, funktioniert dies. Bei schlampigen Entwicklern wird verhindert, dass die Berechnungen zweimal ausgeführt werden, wenn dieselbe Liste aufgerufen wird.

Die akzeptierte Antwort ist richtig, sie bietet die Erklärung, aber nicht die Lösung.

Wenn Sie keine solchen Bibliotheken verwenden, können Sie Folgendes tun:

submitList(null);
submitList(myList);

Eine andere Lösung wäre, die submitList (die nicht so schnell blinkt) als solche zu überschreiben:

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

Oder mit Kotlin-Code:

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

Fragwürdige Logik funktioniert aber perfekt. Meine bevorzugte Methode ist die zweite, da nicht jede Zeile einen onBind-Aufruf erhält.

RJFares
quelle
4
Das ist ein Hack. Übergeben Sie einfach eine Kopie der Liste. .submitList(new ArrayList(list))
Paul Woitaschek
2
Ich habe die letzte Stunde damit verbracht herauszufinden, was das Problem mit meiner Logik ist. So eine komische Logik.
Jerry Okafor
7
@PaulWoitaschek Dies ist kein Hack, dies verwendet JAVA :) Es wird verwendet, um viele Probleme in Bibliotheken zu beheben, in denen der Entwickler "schläft". Der Grund, warum Sie dies wählen, anstatt .submitList (neue ArrayList (Liste)) zu übergeben, liegt darin, dass Sie Listen an mehreren Stellen in Ihrem Code senden können. Möglicherweise vergessen Sie jedes Mal, ein neues Array zu erstellen. Deshalb überschreiben Sie es.
RJFares
1
@ Po10cio Es ist komisch, vor allem, weil angenommen wurde, dass es nur mit ORM-Bibliotheken verwendet wird, die jedes Mal neue Listen anbieten, wenn sie es so geschrieben haben. Wenn Sie dieselbe Liste übergeben, aber aktualisiert haben, müssen Sie das
umgehen
1
Selbst wenn ich Room benutze, stoße ich auf ein ähnliches Problem.
Bink
21

Mit Kotlin müssen Sie lediglich Ihre Liste in eine neue MutableList wie diese oder eine andere Art von Liste konvertieren, je nach Ihrer Verwendung

.observe(this, Observer {
            adapter.submitList(it?.toMutableList())
        })
Mina Samir
quelle
Das ist komisch, aber das Konvertieren der Liste in mutableList funktioniert für mich. Vielen Dank!
Thanh-Nhon Nguyen
3
Warum zum Teufel funktioniert das? Es funktioniert aber sehr neugierig warum das passiert.
3. März, 4.
Meiner Meinung nach darf der ListAdapter nicht über Ihre Listenreferenz sprechen, also senden Sie mit? .toMutableList () eine neue Instanzliste an den Adapter. Ich hoffe das ist klar genug für dich. @ März3April4
Mina Samir
Vielen Dank. Laut Ihrem Kommentar habe ich vermutet, dass der ListAdapter seinen Datensatz als eine Form von List <T> erhält, die eine veränderbare Liste oder sogar eine unveränderliche Liste sein kann. Wenn ich eine unveränderliche Liste verteile, werden die von mir vorgenommenen Änderungen vom Datensatz selbst blockiert, nicht vom ListAdapter.
3.
Ich denke, Sie haben es @ March3April4. Achten Sie auch auf den Mechanismus, den Sie mit den Diff-Utils verwenden, da er auch Verantwortlichkeiten hat. Er berechnet, dass sich die Elemente in der Liste ändern sollten oder nicht;)
Mina Samir
9

Ich hatte ein ähnliches Problem, aber das falsche Rendern wurde durch eine Kombination von setHasFixedSize(true)und verursacht android:layout_height="wrap_content". Zum ersten Mal wurde der Adapter mit einer leeren Liste geliefert, sodass die Höhe nie aktualisiert wurde und wurde 0. Wie auch immer, dies hat mein Problem behoben. Jemand anderes hat möglicherweise das gleiche Problem und glaubt, dass es sich um ein Problem im Adapter handelt.

Jan Veselý
quelle
1
Ja, setzen Sie die Recycling-Ansicht auf wrap_content, um die Liste zu aktualisieren. Wenn Sie sie auf match_parent setzen, wird der Adapter nicht aufgerufen
Exel Staderlin
5

Wenn bei der Verwendung einige Probleme auftreten

recycler_view.setHasFixedSize(true)

Sie sollten diesen Kommentar auf jeden Fall überprüfen: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531

Es hat das Problem auf meiner Seite gelöst.

(Hier ist ein Screenshot des gewünschten Kommentars)

Geben Sie hier die Bildbeschreibung ein

Yoann.G
quelle
Ein Link zu einer Lösung ist willkommen, aber stellen Sie sicher, dass Ihre Antwort ohne sie nützlich ist: Fügen Sie dem Link einen Kontext hinzu, damit Ihre Mitbenutzer eine Vorstellung davon haben, was es ist und warum es dort ist, und zitieren Sie dann den relevantesten Teil der Seite, die Sie verwenden. erneutes Verknüpfen mit, falls die Zielseite nicht verfügbar ist.
Mostafa Arian Nejad
4

Heute bin ich auch auf dieses "Problem" gestoßen. Mit Hilfe der Antwort von insa_c und der Lösung von RJFares habe ich mir eine Kotlin-Erweiterungsfunktion gemacht:

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

die dann überall verwendet werden kann, zB:

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

und es kopiert die Liste nur (und immer), wenn sie mit der aktuell geladenen Liste identisch ist.

Bojan P.
quelle
3

Laut den offiziellen Dokumenten :

Jedes Mal, wenn Sie submitList aufrufen , wird eine neue Liste gesendet, die differenziert und angezeigt werden soll.

Aus diesem Grund wird bei jedem Aufruf von submitList in der vorherigen (bereits übermittelten Liste) der Diff nicht berechnet und der Adapter nicht über Änderungen im Dataset benachrichtigt .

Ashu Tyagi
quelle
2

Für mich trat dieses Problem auf, wenn ich RecyclerViewinnerhalb von ScrollViewmit nestedScrollingEnabled="false"und RV-Höhe auf eingestellt war wrap_content.
Der Adapter wurde ordnungsgemäß aktualisiert und die Bindefunktion wurde aufgerufen, aber die Elemente wurden nicht angezeigt - dieRecyclerView blieb in seiner ursprünglichen Größe hängen.

Ändern, ScrollViewum NestedScrollViewdas Problem zu beheben.

Tomislav
quelle
2

In meinem Fall habe ich vergessen, das LayoutManagerfür das einzustellen RecyclerView. Der Effekt davon ist der gleiche wie oben beschrieben.

just_user
quelle
1

Für jeden, dessen Szenario mit meinem identisch ist, lasse ich hier meine Lösung, von der ich nicht weiß, warum sie funktioniert.

Die Lösung, die für mich funktioniert hat, war von @Mina Samir, die die Liste als veränderbare Liste einreicht.

Mein Problemszenario:

-Laden einer Freundesliste in einem Fragment.

  1. ActivityMain hängt die FragmentFriendList an (beobachtet die Livedata von Friend-DB-Elementen) und fordert gleichzeitig eine http-Anfrage an den Server an, um alle meine Freundeslisten abzurufen.

  2. Aktualisieren oder fügen Sie die Elemente vom http-Server ein.

  3. Jede Änderung löst den onChanged-Rückruf der Livedata aus. Wenn ich die Anwendung zum ersten Mal starte, was bedeutet, dass sich nichts auf meiner Tabelle befand, ist die SubmitList ohne Fehler erfolgreich, aber es wird nichts auf dem Bildschirm angezeigt.

  4. Wenn ich die Anwendung zum zweiten Mal starte, werden Daten auf den Bildschirm geladen.

Die Lösung besteht darin, wie oben beschrieben, die Liste als veränderbare Liste einzureichen.

März3April4
quelle
1

Ich hatte ein ähnliches Problem. Das Problem lag in den DiffFunktionen, die die Elemente nicht angemessen verglichen. Stellen Sie bei Diffjedem Problem sicher, dass Ihre Funktionen (und damit auch Ihre Datenobjektklassen) die richtigen Vergleichsdefinitionen enthalten, dh vergleichen Sie alle Felder, die möglicherweise im neuen Element aktualisiert werden. Zum Beispiel im Originalbeitrag

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

Diese Funktion macht (möglicherweise) nicht das, was auf dem Etikett steht: Sie vergleicht nicht den Inhalt der beiden Elemente - es sei denn, Sie haben die equals()Funktion in der ArtistKlasse überschrieben . In meinem Fall hatte ich nicht und die Definition von areContentsTheSamenur eines der notwendigen Felder überprüft, aufgrund meiner Kontrolle bei der Implementierung. Dies ist strukturelle Gleichheit vs. referentielle Gleichheit. Mehr dazu finden Sie hier

Ampalmer
quelle
0

Ich musste meine DiffUtils ändern

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

Um tatsächlich zurückzugeben, ob der Inhalt neu ist, vergleichen Sie nicht nur die ID des Modells.

Tonisive
quelle
0

Wenn Sie die erste Antwort von @RJFares verwenden, wird die Liste erfolgreich aktualisiert, der Bildlaufstatus wird jedoch nicht beibehalten. Das Ganze RecyclerViewstartet von der 0. Position. Um dies zu umgehen, habe ich Folgendes getan:

   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

   }

Auf diese Weise kann ich den Bildlaufstatus RecyclerViewzusammen mit geänderten Daten beibehalten .

iCantC
quelle