ListAdapter ne met pas à jour l'élément dans RecyclerView

89

J'utilise la nouvelle bibliothèque de support ListAdapter. Voici mon code pour l'adaptateur

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

Je mets à jour l'adaptateur avec

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

Ma classe de 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
    }
}

Ce qui se passe, c'est lorsque submitList est appelé la première fois que l'adaptateur rend tous les éléments, mais lorsque submitList est à nouveau appelé avec des propriétés d'objet mises à jour, il ne rend pas la vue qui a changé.

Il restitue la vue lorsque je fais défiler la liste, qui à son tour appelle bindView()

En outre, j'ai remarqué que l'appel adapter.notifyDatasSetChanged()après la liste de soumission rend la vue avec des valeurs mises à jour, mais je ne veux pas appeler notifyDataSetChanged()car l'adaptateur de liste a des utilitaires différentiels intégrés

Quelqu'un peut-il m'aider?

Veeresh Charantimath
la source
Le problème pourrait être lié à ArtistsDiffet donc à sa mise en œuvre Artist.
tynn le
Oui, moi aussi je pense la même chose, mais je n'arrive pas à le
préciser
Vous pouvez le déboguer ou ajouter des instructions de journal. Vous pouvez également ajouter le code correspondant à la question.
tynn le
vérifiez également cette question, je l'ai résolue différemment stackoverflow.com/questions/58232606/…
MisterCat

Réponses:

100

Edit: Je comprends pourquoi cela arrive, ce n'était pas mon propos. Mon point est qu'il doit au moins donner un avertissement ou appeler la notifyDataSetChanged()fonction. Parce qu'apparemment j'appelle la submitList(...)fonction pour une raison. Je suis sûr que les gens essaient de comprendre ce qui n'a pas fonctionné pendant des heures jusqu'à ce qu'ils découvrent que submitList () ignore silencieusement l'appel.

Ceci est dû à Googleune logique étrange. Donc, si vous passez la même liste à l'adaptateur, il n'appelle même pas le DiffUtil.

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

Je ne comprends vraiment pas tout l'intérêt de cela ListAdapters'il ne peut pas gérer les changements sur la même liste. Si vous souhaitez modifier les éléments de la liste que vous passez à ListAdapteret voir les modifications, vous devez soit créer une copie complète de la liste, soit utiliser Regular RecyclerViewavec votre propre DiffUtillclasse.

insa_c
la source
5
Parce qu'il nécessite l'état précédent pour effectuer la diff. Bien sûr, il ne peut pas le gérer si vous écrasez l'état précédent. O_o
EpicPandaForce
29
Oui mais à ce stade, il y a une raison pour laquelle j'appelle le submitList, non? Il devrait au moins appeler le notifyDataSetChanged()au lieu d'ignorer silencieusement l'appel. Je suis presque sûr que les gens essaient de comprendre ce qui n'a pas fonctionné pendant des heures jusqu'à ce qu'ils découvrent qu'ils submitList()ignorent silencieusement l'appel.
insa_c
5
Donc je suis de retour à RecyclerView.Adapter<VH>et notifyDataSetChanged(). La vie est bonne maintenant. Bon nombre d'heures gaspillé
Udayaditya Barua
@insa_c Vous pouvez ajouter 3 heures à votre décompte, c'est tout ce que j'ai perdu à essayer de comprendre pourquoi ma liste ne se mettait pas à jour dans certains cas
extrêmes
1
notifyDataSetChanged()est coûteux et irait complètement à l'encontre de l'intérêt d'une implémentation basée sur DiffUtil. Vous pouvez être prudent et attentif en n'appelant submitListqu'avec de nouvelles données, mais en réalité, ce n'est qu'un piège de performance.
David Liu
62

La bibliothèque suppose que vous utilisez Room ou tout autre ORM qui offre une nouvelle liste asynchrone à chaque fois qu'elle est mise à jour, donc appeler simplement submitList dessus fonctionnera, et pour les développeurs bâclés, cela empêche de faire les calculs deux fois si la même liste est appelée.

La réponse acceptée est correcte, elle offre l'explication mais pas la solution.

Ce que vous pouvez faire si vous n'utilisez pas de telles bibliothèques est:

submitList(null);
submitList(myList);

Une autre solution serait de remplacer submitList (qui ne provoque pas ce clignotement rapide) comme tel:

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

Ou avec le code Kotlin:

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

Logique discutable mais fonctionne parfaitement. Ma méthode préférée est la deuxième car elle ne provoque pas chaque ligne pour obtenir un appel onBind.

RJFares
la source
4
C'est un hack. Passez simplement une copie de la liste. .submitList(new ArrayList(list))
Paul Woitaschek
2
J'ai passé la dernière heure à essayer de comprendre quel est le problème avec ma logique. Une logique tellement étrange.
Jerry Oka pour
7
@PaulWoitaschek Ce n'est pas un hack, cela utilise JAVA :) il est utilisé pour résoudre de nombreux problèmes dans les bibliothèques où le développeur «dort». La raison pour laquelle vous choisiriez ceci au lieu de transmettre .submitList (new ArrayList (list)) est que vous pouvez soumettre des listes à plusieurs endroits dans votre code. Vous pourriez oublier de créer un nouveau tableau à chaque fois, c'est pourquoi vous remplacez.
RJFares
1
@ Po10cio C'est bizarre principalement parce que quand ils l'ont écrit comme ça, on a supposé qu'il ne serait utilisé qu'avec des bibliothèques ORM qui offrent de nouvelles listes à chaque fois. Si vous passez la même liste mais mise à jour, vous devez contourner cela, et ce serait le meilleur moyen
RJFares
1
Même avec l'utilisation de Room, je rencontre un problème similaire.
Bink
21

avec Kotlin, il vous suffit de convertir votre liste en une nouvelle MutableList comme celle-ci ou un autre type de liste en fonction de votre utilisation

.observe(this, Observer {
            adapter.submitList(it?.toMutableList())
        })
Mina Samir
la source
C'est bizarre mais convertir la liste en mutableList fonctionne pour moi. Merci!
Thanh-Nhon Nguyen
3
Pourquoi diable ça marche? Cela fonctionne mais très curieux de savoir pourquoi cela se produit.
mars 4
à mon avis, le ListAdapter ne doit pas faire sur votre référence de liste, donc avec ça? .toMutableList () vous soumettez une nouvelle liste d'instances à l'adaptateur. J'espère que c'est assez clair pour vous. @ March3April4
Mina Samir
Merci. D'après votre commentaire, j'ai deviné que le ListAdapter reçoit son ensemble de données sous forme de List <T>, qui peut être une liste mutable, voire une liste immuable. Si je passe une liste immuable, les modifications que j'ai apportées sont bloquées par l'ensemble de données lui-même, pas par le ListAdapter.
3
Je pense que vous l'avez compris @ March3April4 Aussi, faites attention au mécanisme que vous utilisez avec les diff utils car il a également des responsabilités pour calculer les éléments de la liste qui doivent changer ou non;)
Mina Samir
9

J'ai eu un problème similaire mais le rendu incorrect a été causé par une combinaison de setHasFixedSize(true)et android:layout_height="wrap_content". Pour la première fois, l'adaptateur a été fourni avec une liste vide de sorte que la hauteur n'a jamais été mise à jour et l'était 0. Quoi qu'il en soit, cela a résolu mon problème. Quelqu'un d'autre pourrait avoir le même problème et pensera qu'il s'agit d'un problème dans l'adaptateur.

Jan Veselý
la source
1
Oui, définissez recycleview sur wrap_content mettra à jour la liste, si vous le définissez sur match_parent, il n'appellera pas l'adaptateur
Exel Staderlin
5

Si vous rencontrez des problèmes lors de l'utilisation

recycler_view.setHasFixedSize(true)

vous devriez certainement vérifier ce commentaire: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531

Cela a résolu le problème de mon côté.

(Voici une capture d'écran du commentaire comme demandé)

entrez la description de l'image ici

Yoann.G
la source
Un lien vers une solution est le bienvenu, mais veuillez vous assurer que votre réponse est utile sans elle: ajoutez du contexte autour du lien afin que vos collègues utilisateurs aient une idée de ce que c'est et pourquoi il est là, puis citez la partie la plus pertinente de la page que vous '' relier au cas où la page cible ne serait pas disponible.
Mostafa Arian Nejad
4

Aujourd'hui, je suis également tombé sur ce "problème". Avec l'aide de la réponse d' insa_c et de la solution RJFares je me suis créé une fonction d'extension 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)
}

qui peut ensuite être utilisé n'importe où, par exemple:

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

et il ne copie (et toujours) la liste que si elle est la même que celle actuellement chargée.

Bojan P.
la source
3

Selon les documents officiels :

Chaque fois que vous appelez submitList, il soumet une nouvelle liste à comparer et à afficher.

C'est pourquoi chaque fois que vous appelez submitList sur la liste précédente (liste déjà soumise), il ne calcule pas le Diff et ne notifie pas l'adaptateur en cas de modification de l'ensemble de données.

Ashu Tyagi
la source
2

Pour moi, ce problème est apparu si j'utilisais à l' RecyclerViewintérieur de ScrollViewavec nestedScrollingEnabled="false"et la hauteur RV définie sur wrap_content.
L'adaptateur s'est mis à jour correctement et la fonction de liaison a été appelée, mais les éléments n'ont pas été affichés - le RecyclerViewétait bloqué à sa «taille d'origine.

Passer ScrollViewàNestedScrollView résoudre le problème.

Tomislav
la source
2

Dans mon cas, j'ai oublié de définir le LayoutManagerpour le RecyclerView. L'effet est le même que celui décrit ci-dessus.

just_user
la source
1

Pour tous ceux dont le scénario est le même que le mien, je laisse ma solution, dont je ne sais pas pourquoi elle fonctionne, ici.

La solution qui a fonctionné pour moi était de @Mina Samir, qui soumet la liste sous forme de liste modifiable.

Mon scénario de problème:

-Chargement d'une liste d'amis dans un fragment.

  1. ActivityMain attache la FragmentFriendList (observe les données de vie des éléments de base de données ami) et en même temps, demande une requête http au serveur pour obtenir toute ma liste d'amis.

  2. Mettez à jour ou insérez les éléments du serveur http.

  3. Chaque changement déclenche le rappel onChanged de la liveata. Mais, quand c'est la première fois que je lance l'application, ce qui signifie qu'il n'y avait rien sur ma table, la submitList réussit sans aucune erreur d'aucune sorte, mais rien n'apparaît à l'écran.

  4. Cependant, lorsque c'est la deuxième fois que je lance l'application, les données sont chargées à l'écran.

La solution est, comme indiqué ci-dessus, de soumettre la liste en tant que mutableList.

Mars3avril4
la source
1

J'avais un problème similaire. Le problème était dans les Difffonctions, qui ne comparaient pas correctement les éléments. Toute personne ayant ce problème, assurez-vous que vos Difffonctions (et par extension vos classes d'objets de données) contiennent des définitions de comparaison appropriées - c'est-à-dire en comparant tous les champs qui pourraient être mis à jour dans le nouvel élément. Par exemple dans le message d'origine

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

Cette fonction (potentiellement) ne fait pas ce qu'elle dit sur l'étiquette: elle ne compare pas le contenu des deux éléments - sauf si vous avez remplacé la equals()fonction dans la Artistclasse. Dans mon cas, je ne l'avais pas fait, et la définition de areContentsTheSamen'a coché qu'un des champs nécessaires, en raison de mon oubli lors de sa mise en œuvre. C'est l'égalité structurelle vs l'égalité référentielle, vous pouvez en savoir plus ici

ampalmer
la source
0

J'avais besoin de modifier mes DiffUtils

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

Pour retourner réellement si le contenu est nouveau, pas simplement comparer l'id du modèle.

tonisifs
la source
0

L'utilisation de la première réponse @RJFares met à jour la liste avec succès, mais ne conserve pas l'état de défilement. L'ensemble RecyclerViewcommence à partir de la 0ème position. Pour contourner ce problème, voici ce que j'ai fait:

   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

   }

De cette façon, je suis capable de maintenir l'état de défilement RecyclerViewavec les données modifiées.

iCantC
la source