ListAdapter non aggiorna l'elemento in RecyclerView


92

Sto usando la nuova libreria di supporto ListAdapter. Ecco il mio codice per l'adattatore

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

Sto aggiornando l'adattatore con

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

La mia classe di 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
    }
}

Ciò che accade è quando submitList viene chiamato la prima volta che l'adattatore esegue il rendering di tutti gli elementi, ma quando submitList viene chiamato di nuovo con le proprietà dell'oggetto aggiornate, non esegue nuovamente il rendering della vista che è cambiata.

Rende nuovamente la vista mentre scorro l'elenco, che a sua volta chiama bindView()

Inoltre, ho notato che la chiamata adapter.notifyDatasSetChanged()dopo la lista di invio rende la vista con valori aggiornati, ma non voglio chiamarenotifyDataSetChanged() perché l'adattatore dell'elenco ha diff utils integrato

Qualcuno può aiutarmi qui?


Il problema potrebbe essere correlato ArtistsDiffe quindi alla realizzazione di Artistse stesso.
tynn

Sì, anche io penso lo stesso, ma non riesco a individuarlo
Veeresh Charantimath

Puoi eseguire il debug o aggiungere istruzioni di log. Inoltre potresti aggiungere il codice pertinente alla domanda.
tynn

controllare anche questa domanda, ho risolto in modo diverso stackoverflow.com/questions/58232606/...
MisterCat

Risposte:


108

Modifica: capisco perché questo accade che non era il mio punto. Il punto è che deve almeno dare un avviso o chiamare la notifyDataSetChanged()funzione. Perché a quanto pare sto chiamando la submitList(...)funzione per un motivo. Sono abbastanza sicuro che le persone stiano cercando di capire cosa è andato storto per ore fino a quando non capiscono che submitList () ignora silenziosamente la chiamata.

Ciò è dovuto alla Googlestrana logica. Quindi, se si passa lo stesso elenco all'adattatore, non viene nemmeno chiamato il file DiffUtil.

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

Non capisco davvero l'intero punto di questo ListAdapterse non può gestire le modifiche sullo stesso elenco. Se si desidera modificare gli elementi nell'elenco che si passa a ListAdaptere vedere le modifiche, è necessario creare una copia completa dell'elenco o è necessario utilizzarlo regolarmente RecyclerViewcon la propria DiffUtillclasse.


5
Perché richiede lo stato precedente per eseguire il diff. Ovviamente non può gestirlo se sovrascrivi lo stato precedente. O_o
EpicPandaForce

34
Si ma a quel punto c'è un motivo per cui chiamo il submitList, giusto? Dovrebbe almeno chiamare il notifyDataSetChanged()invece di ignorare silenziosamente la chiamata. Sono abbastanza sicuro che le persone stiano cercando di capire cosa è andato storto per ore finché non capiscono che submitList()ignora silenziosamente la chiamata.
insa_c

6
Quindi sono tornato a RecyclerView.Adapter<VH>e notifyDataSetChanged(). La vita va bene adesso. Buon numero di ore sprecate
Udayaditya Barua

1
@insa_c Puoi aggiungere 3 ore al tuo conteggio, è quanto ho sprecato cercando di capire perché la mia listview non si aggiornava in alcuni casi limite ...
Bencri

1
notifyDataSetChanged()è costoso e vanificherebbe completamente il punto di avere un'implementazione basata su DiffUtil. Potresti essere attento e intenzionato a chiamare submitListsolo con nuovi dati, ma in realtà è solo una trappola delle prestazioni.
David Liu

63

La libreria presuppone che tu stia utilizzando Room o qualsiasi altro ORM che offre un nuovo elenco asincrono ogni volta che viene aggiornato, quindi la chiamata a submitList su di essa funzionerà e, per gli sviluppatori sciatti, impedisce di eseguire i calcoli due volte se viene chiamato lo stesso elenco.

La risposta accettata è corretta, offre la spiegazione ma non la soluzione.

Quello che puoi fare nel caso in cui non utilizzi nessuna di queste librerie è:

submitList(null);
submitList(myList);

Un'altra soluzione potrebbe essere l'override di submitList (che non causa quel lampo veloce) come tale:

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

O con il codice Kotlin:

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

Logica discutibile ma funziona perfettamente. Il mio metodo preferito è il secondo perché non fa sì che ogni riga riceva una chiamata onBind.


7
È un trucco. Basta passare una copia dell'elenco. .submitList(new ArrayList(list))
Paul Woitaschek

3
Ho passato l'ultima ora a cercare di capire qual è il problema con la mia logica. Una logica così strana.
Jerry Oka per il

7
@PaulWoitaschek Questo non è un hack, utilizza JAVA :) è usato per risolvere molti problemi nelle librerie in cui lo sviluppatore sta "dormendo". Il motivo per cui scegli questo invece di passare .submitList (new ArrayList (list)) è perché puoi inviare elenchi in più punti del tuo codice. Potresti dimenticare di creare un nuovo array ogni volta, ecco perché esegui l'override.
RJFares

2
Anche utilizzando Room sto riscontrando un problema simile.
Bink

2
Apparentemente questo funziona quando aggiorno un elenco in viewmodel con nuovi elementi, ma quando aggiorno una proprietà (boolean - isSelected) di un elemento in un elenco, questo non funzionerà ancora. Idk why ma DiffUtil restituisce lo stesso vecchio e nuovo elemento di I ' ho controllato. Qualche idea su dove potrebbe verificarsi il problema?
Ralph

23

con Kotlin devi solo convertire il tuo elenco in un nuovo Elenco mutevole come questo o un altro tipo di elenco in base al tuo utilizzo

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

È strano ma convertire l'elenco in mutableList funziona per me. Grazie!
Thanh-Nhon Nguyen,

4
Perché diavolo funziona? Funziona ma è molto curioso perché questo accade.
marzo3 aprile4

a mio parere, ListAdapter non deve fare riferimento al tuo elenco di riferimento, quindi con esso? .toMutableList () invii un nuovo elenco di istanze all'adattatore. Spero che sia abbastanza chiaro per te. @ March3April4
Mina Samir

Grazie. Secondo il tuo commento, ho immaginato che ListAdapter riceva il suo set di dati come una forma di List <T>, che può essere un elenco modificabile o anche un elenco immutabile. Se distribuisco un elenco immutabile, le modifiche apportate vengono bloccate dal set di dati stesso, non da ListAdapter.
3

Penso che tu l'abbia capito @ March3April4 Inoltre, preoccupati del meccanismo che usi con le utility diff perché ha anche delle responsabilità calcolerà gli elementi nell'elenco dovrebbero cambiare o no;)
Mina Samir

10

Ho avuto un problema simile ma il rendering errato è stato causato da una combinazione di setHasFixedSize(true)e android:layout_height="wrap_content". Per la prima volta l'adattatore è stato fornito con una lista vuota, quindi l'altezza non è mai stata aggiornata ed è stata 0. Comunque, questo ha risolto il mio problema. Qualcun altro potrebbe avere lo stesso problema e penserà che sia un problema nell'adattatore.


1
Sì, imposta il recycleview su wrap_content aggiornerà l'elenco, se lo imposti su match_parent non chiamerà l'adattatore
Exel Staderlin

5

Oggi sono incappato anche in questo "problema". Con l'aiuto della risposta di insa_c e della soluzione di RJFares mi sono creato una funzione di estensione 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)
}

che può quindi essere utilizzato ovunque, ad esempio:

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

e copia l'elenco solo (e sempre) se è uguale a quello attualmente caricato.


5

In caso di problemi durante l'utilizzo di

recycler_view.setHasFixedSize(true)

dovresti assolutamente controllare questo commento: https://github.com/ thoughttbot/expandable-recycler-view/issues/53#issuecomment- 362991531

Ha risolto il problema dalla mia parte.

(Ecco uno screenshot del commento come richiesto)

inserisci qui la descrizione dell'immagine


Un collegamento a una soluzione è il benvenuto, ma assicurati che la tua risposta sia utile senza di essa: aggiungi un contesto attorno al collegamento in modo che i tuoi colleghi utenti abbiano un'idea di cosa sia e perché sia ​​lì, quindi cita la parte più pertinente della pagina che tu " re collegamento a nel caso in cui la pagina di destinazione non è disponibile.
Mostafa Arian Nejad

3

Secondo i documenti ufficiali :

Ogni volta che si chiama submitList, viene inviato un nuovo elenco da modificare e visualizzare.

Questo è il motivo per cui ogni volta che si chiama submitList sulla lista precedente (già inviata), non calcola il Diff e non notifica all'adattatore la modifica del set di dati.


3

Nel mio caso ho dimenticato di impostare il LayoutManagerper RecyclerView. L'effetto di ciò è lo stesso descritto sopra.


2

Per me, questo problema si verificava se stavo usando l' RecyclerViewinterno di ScrollViewcon nestedScrollingEnabled="false"e l'altezza RV impostata su wrap_content.
L'adattatore è stato aggiornato correttamente ed è stata chiamata la funzione di associazione, ma gli elementi non sono stati mostrati: RecyclerViewera bloccata alla sua dimensione originale.

Modifica ScrollViewper NestedScrollViewrisolvere il problema.


2

Ho avuto un problema simile. Il problema era nelle Difffunzioni, che non confrontavano adeguatamente gli elementi. Chiunque abbia questo problema, assicurati che il tuoDiff funzioni (e per estensione le tue classi di oggetti dati) contengano definizioni di confronto appropriate, ovvero confrontando tutti i campi che potrebbero essere aggiornati nel nuovo elemento. Ad esempio nel post originale

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

Questa funzione (potenzialmente) non fa quello che dice sull'etichetta: non confronta il contenuto dei due elementi - a meno che tu non abbia sovrascritto la equals()funzione nella Artistclasse. Nel mio caso, non l'avevo fatto e la definizione di areContentsTheSamecontrollato solo uno dei campi necessari, a causa della mia supervisione durante l'implementazione. Questa è l'uguaglianza strutturale contro l'uguaglianza referenziale, puoi trovare di più a riguardo qui


1

Per chiunque abbia uno scenario uguale al mio, lascio qui la mia soluzione, che non so perché funziona.

La soluzione che ha funzionato per me è stata di @Mina Samir, che presenta l'elenco come elenco modificabile.

Scenario del mio problema:

-Caricamento di una lista di amici all'interno di un frammento.

  1. ActivityMain allega FragmentFriendList (Observes to the livesata of friend db items) e, contemporaneamente, richiede una richiesta http al server per ottenere tutta la mia lista di amici.

  2. Aggiorna o inserisci gli elementi dal server http.

  3. Ogni modifica accende il callback onChanged del livesata. Ma, quando è la prima volta che avvio l'applicazione, il che significa che non c'era nulla sul mio tavolo, submitList riesce senza errori di alcun tipo, ma sullo schermo non appare nulla.

  4. Tuttavia, quando è la seconda volta che avvio l'applicazione, i dati vengono caricati sullo schermo.

La soluzione è, come indicato sopra, presentare l'elenco come mutableList.


1

Il motivo per cui il tuo ListAdapter .submitlist non viene chiamato è perché l'oggetto che hai aggiornato conserva ancora lo stesso indirizzo in memoria.

Quando aggiorni un oggetto con diciamo .setText, cambia il valore nell'oggetto originale.

In modo che quando controlli se object.id == object2.id tornerà uguale perché entrambi hanno un riferimento alla stessa posizione in memoria.

La soluzione è creare un nuovo oggetto con i dati aggiornati e inserirlo nell'elenco. Quindi verrà chiamato submitList e funzionerà correttamente


0

Avevo bisogno di modificare i miei DiffUtils

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

Per restituire effettivamente se i contenuti sono nuovi, non basta confrontare l'id del modello.


0

L'utilizzo della prima risposta di @RJFares aggiorna correttamente l'elenco, ma non mantiene lo stato di scorrimento. L'intero RecyclerViewparte dalla posizione 0. Come soluzione alternativa, questo è quello che ho fatto:

   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

   }

In questo modo, sono in grado di mantenere lo stato di scorrimento RecyclerViewinsieme ai dati modificati.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.