8
votes

Quel est le problème avec mon implémentation DiffUtil?

Mise à jour : l'un des problèmes est résolu: maintenant updateList est résolu, le problème était que j'ai défini mAdapter comme RecyclerView .Adapter au lieu de MyAdapter . Mais maintenant, même si je reçois des données, rien n'apparaît dans la liste, elle est vide

-------------------- ORIGINAL POST --------------------

Je souhaite mettre à jour mon RecyclerView en utilisant DiffUtil pour éviter les doublons.

J'ai 4 classes: la classe User, la classe Activity où je définis les données, la classe Adapter et le code DiffUtil > classe. Je ne suis pas sûr de combiner tous ces 4 correctement.

Voici la classe User:

public class MyFragment extends Fragment {
    private ArrayList<User> myDataset;
    private RecyclerView.Adapter mAdapter;

    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        // Inflate the layout for this fragment
        rootView = inflater.inflate(R.layout.fragment_lks, container, false);

        mRecyclerView = (RecyclerView) rootView.findViewById(R.id.my_recycler_view);
        myDataset = new ArrayList<User>();
        mAdapter = new MyAdapter(myDataset);

C'est ainsi que je définit les données dynamiquement (je continue à de nouveaux tableaux Json du serveur contenant les identifiants utilisateur à afficher, puis j'ai défini l'image utilisateur à partir du stockage Firebase ): (C'est une fonction appelée par un auditeur onClick:)

Ceci est l'appel de méthode depuis le fragment:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private ArrayList<User> mDataset;
    private MyViewHolder myHolder;
    private User user;

    public static class MyViewHolder extends RecyclerView.ViewHolder {

        public TextView singleItemTextView;
        public ImageView singleItemImage;
        public View layout;
        public ConstraintLayout constraintLayout;
        public MyViewHolder(View v) {
            super(v);
            layout = v;
            singleItemImage = (ImageView) v.findViewById(R.id.icon);
            singleItemTextView = (TextView) v.findViewById(R.id.singleitemtv);
            constraintLayout = (ConstraintLayout) v.findViewById(R.id.nbConstraintLayout);
        }
    }

    // Provide a suitable constructor (depends on the kind of dataset)
    public MyAdapter(ArrayList<User> myDataset) {
        mDataset = myDataset;
    }

    // Create new views (invoked by the layout manager)
    @Override
    public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
                                                     int viewType) {


        View v =  LayoutInflater.from(parent.getContext())
                .inflate(R.layout.nb_image_view, parent, false);

        MyViewHolder vh = new MyViewHolder(v);
        return vh;
    }

    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position) {
        myHolder = holder;


        user = mDataset.get(position);
        Uri userImage = user.getImageUrl();       
        myHolder.singleItemTextView.setText(user.getUserId());

        Glide.with(myHolder.itemView.getContext() /* context */)
                .load(userImage)
                .into(myHolder.singleItemImage);
        myHolder.constraintLayout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {               
                 Context context = v.getContext();                
                Intent intent = new Intent(v.getContext(), DisplayUserActivity.class);
              context.startActivity(intent);
            }
        });

    }
    public void updateList(ArrayList<User> newList) {
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffUtilCallBack(this.mDataset, newList));
        diffResult.dispatchUpdatesTo(this);
    }
}

Voici la fonction:

public class MyDiffUtilCallBack extends DiffUtil.Callback{

    ArrayList<User> oldUsers;
    ArrayList<User> newUsers;

    public MyDiffUtilCallBack(ArrayList<User> newUsers, ArrayList<User> oldUsers) {
        this.newUsers = newUsers;
        this.oldUsers = oldUsers;
    }

    @Override
    public int getOldListSize() {
        return oldUsers.size();
    }

    @Override
    public int getNewListSize() {
        return newUsers.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return oldUsers.get(oldItemPosition).getUserId().equals( newUsers.get(newItemPosition).getUserId());
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return oldUsers.get(oldItemPosition).equals(newUsers.get(newItemPosition));
    }

    @Nullable
    @Override
    public Object getChangePayload(int oldItemPosition, int newItemPosition) {
        //you can return particular field for changed item.
        return super.getChangePayload(oldItemPosition, newItemPosition);
    }
}

Voici à quoi ressemble ma classe DiffUtil :

   private void updateUsersList() {
        @Override
        public void onResponse(JSONArray response) { // the JSON ARRAY response of user ids ["uid1", "uid334", "uid1123"]
            myDataset.clear(); // clear dataset to prevent duplicates
            for (int i = 0; i < response.length(); i++) {
                try {
                    String userKey = response.get(i).toString(); // the currently iterated user id
                    final DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
                    DatabaseReference userKeyRef = rootRef.child("users").child(userKey); // reference to currently iterated user
                    ValueEventListener listener = new ValueEventListener() {
                    @Override
                    public void onDataChange(DataSnapshot dataSnapshot) {
                        myDataset.add(new User(dataSnapshot.getKey(), dataSnapshot.child("imageUrl").getValue().toString())); //add new user: id and image url
                        mAdapter.updateList(myDataset); // cannot resolve this method, why?
                   }
                   @Override
                   public void onCancelled(@NonNull DatabaseError databaseError) {
                   Log.d(TAG, databaseError.getMessage());
                   }
                  };
                  userKeyRef.addListenerForSingleValueEvent(listener);
              }
              catch (JSONException e) { Log.d(TAG, "message " + e); }
           }
   }

Et voici mon adaptateur:

button.setOnClickListener(new View.OnClickListener() {
    public void onClick(View v) {               
        updateUsersList();
    }
});

Je ne suis pas sûr de combiner correctement toutes les classes (ma première fois en utilisant DiffUtil ), et j'obtiens également impossible de résoudre la méthode updateList (?)

Qu'est-ce que je fais mal?

Voici comment je définis mAdapter dans mon Fragment:

public class User {

    private String mUserId;
    private Uri mImageUrl;


    public User(String userId, String imageUrl) {
        mUserId = userId;
        mImageUrl = Uri.parse(imageUrl);
    }


    public String getUserId() {
        return mUserId;
    }

    public Uri getImageUrl() {
        return mImageUrl;
    }
}


5 commentaires

Cela semble correct. Veuillez publier la définition de mAdapter ?


ajoutée. Au fait, la récupération des données se trouve dans une méthode appelée par un événement onClick, je vais éditer le message pour l'expliquer. Pour une raison quelconque, la updateList n'est pas visible sur le mAdapter: /


Avez-vous redéfini est égal à pour votre classe User ? Sans cela, areContentsTheSame ne fonctionnera pas correctement.


Tu as raison, rien ne s'affiche, tu crois que c'est parce que les égaux? Pouvez-vous s'il vous plaît être plus précis? Je suis nouveau sur Android donc je ne sais pas comment le redéfinir


@ThibaultSeisel pouvez-vous s'il vous plaît ajouter comment dois-je le changer? Je ne peux pas le faire fonctionner


4 Réponses :


7
votes

Le problème vient de la définition de mAdapter . Vous l'avez défini comme RecyclerView.Adapter qui est la super classe de votre MyAdapter et il ne contient pas updateList () . Vous devez le modifier comme suit:

public class User {

    private String mUserId;
    private Uri mImageUrl;

    public User(String userId, String imageUrl) {
        mUserId = userId;
        mImageUrl = Uri.parse(imageUrl);
    }

    public String getUserId() {
        return mUserId;
    }

    public Uri getImageUrl() {
        return mImageUrl;
    }

    @Override
    public boolean equals(Object other) {
        if (other instanceof User) {
            User user = (User) other;
            return mUserId.equals(user.getUserId()) && mImageUrl.equals(user.getImageUrl());
        } else {
            return false;
        }
    }

}

Mise à jour 13/01/2019:

J'ai réécrit votre adaptateur avec AsyncListDiffer qui calcule la différence de manière asynchrone puis l'applique à l'adaptateur.

MyAdapter.java

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.constraint.ConstraintLayout;
import android.support.v7.recyclerview.extensions.AsyncListDiffer;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;   
import com.bumptech.glide.Glide;    
import java.util.List;


public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private AsyncListDiffer<User> mAsyncListDiffer;

    public static class MyViewHolder extends RecyclerView.ViewHolder {

        public TextView singleItemTextView;
        public ImageView singleItemImage;
        public View layout;
        public ConstraintLayout constraintLayout;

        public MyViewHolder(View v) {
            super(v);
            layout = v;
            singleItemImage = (ImageView) v.findViewById(R.id.icon);
            singleItemTextView = (TextView) v.findViewById(R.id.singleitemtv);
            constraintLayout = (ConstraintLayout) v.findViewById(R.id.nbConstraintLayout);
        }
    }

    // Provide a suitable constructor (depends on the kind of dataset)
    public MyAdapter() {
        DiffUtil.ItemCallback<User> diffUtilCallback = new DiffUtil.ItemCallback<User>() {

            @Override
            public boolean areItemsTheSame(@NonNull User newUser, @NonNull User oldUser) {
                return newUser.getUserId().equals(oldUser.getUserId());
            }

            @Override
            public boolean areContentsTheSame(@NonNull User newUser, @NonNull User oldUser) {
                return newUser.equals(oldUser);
            }
        };
        mAsyncListDiffer = new AsyncListDiffer<>(this, diffUtilCallback);
    }

    // Create new views (invoked by the layout manager)
    @Override
    public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.nb_image_view, parent, false);
        MyViewHolder vh = new MyViewHolder(v);
        return vh;
    }

    @Override
    public void onBindViewHolder(final MyViewHolder holder, final int position) {
        User user = mAsyncListDiffer.getCurrentList().get(position);
        Uri userImage = user.getImageUrl();
        holder.singleItemTextView.setText(user.getUserId());

        Glide.with(holder.itemView.getContext() /* context */)
                .load(userImage)
                .into(holder.singleItemImage);

        holder.constraintLayout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Context context = v.getContext();
                Intent intent = new Intent(v.getContext(), DisplayUserActivity.class);
                context.startActivity(intent);
            }
        });
    }

    @Override
    public int getItemCount() {
        return mAsyncListDiffer.getCurrentList().size();
    }

    public void updateList(List<User> newList) {
        mAsyncListDiffer.submitList(newList);
    }

}

User.java

private MyAdapter mAdapter;


19 commentaires

Merci, maintenant 1 problème est résolu: la updateList est visible, mais maintenant la liste ne montrera plus rien (rien n'apparaît bien que j'obtienne des données). Lorsque je le teste avec notifyDataSetChanged , il affiche les données


Vous êtes les bienvenus. Je pense qu'il vaut mieux définir public MyAdapter () au lieu de public MyAdapter (ArrayList myDataset) et dans MyAdapter définir et instancier < code> mDataset as private ArrayList mDataset = new ArrayList (); . Cela peut résoudre le problème.


vous voulez dire laisser le public MyAdapter () {} vide? Et qu'en est-il de l'autre commentaire? À propos de la substitution de la méthode equals dans ma classe User ? J'ai fait ce que vous avez dit, et il n'affiche toujours rien: (Notez également qu'il y a mDataset et myDataset à ne pas confondre avec les deux


Pourriez-vous s'il vous plaît modifier votre réponse avec des correctifs possibles pour que je sache exactement quoi faire? Merci


Oui, je veux dire qu'il ne lui passe pas la liste des utilisateurs. Cela conduit à un mDataset vide la première fois, puis lorsqu'une liste est extraite de la base de données, DiffUtil calcule la différence entre une liste vide et une liste récupérée qui conduit à afficher tous les nouvelles données. Bien sûr, je vais le modifier.


Ok, j'ai fait exactement ça, mais rien n'apparaît, il nous manque quelque chose. Quelqu'un dans les commentaires a mentionné quelque chose sur le remplacement de equals dans la classe User , ce que je n'ai pas fait, et je ne sais pas comment le faire. Mais je ne sais pas si cela va résoudre le problème. Que nous manque-t-il? updateList est censé remplacer complètement notifyDataSetChanged ?


Oui, il y a un bogue dans areItemsTheSame . Lorsque vous avez défini mUserId comme String , vous devez vérifier l'égalité en utilisant est égal à , et non == . Nous avons donc: return oldUsers.get (oldItemPosition) .getUserId (). Equals (newUsers.ge‌ t (newItemPosition) .g‌ etUserId ()); J'édite la réponse avec mis à jour Classe utilisateur . Cela peut résoudre le problème comme vous le pensez.


La updateList mise à jour n'est pas égale à notifyDataSetChanged . Parce que le plus gros de la mise à jour de l'ensemble des éléments est de réaffecter toutes les valeurs aux vues. Il remplace simplement les données ArrayList et en utilisant DiffUtil il met à jour les seules vues requises.


En fait, j'ai déjà changé cette partie en est égal à , mais il a mentionné autre chose, remplaçant égal à areContentsTheSame . Rien n'apparaît. peut-être que je dois appeler au moins une fois onDataSetChanged dans un premier temps?


onDataSetChanged n'est pas du tout nécessaire. Est-il possible de partager plus de code du fragment, ou mieux de partager le projet dans un dépôt github privé et de m'y ajouter.


Le projet est en désordre, car je suis débutant, il n'y a vraiment pas grand chose à ajouter. À propos, lorsque je connecte myDataset à chaque itération, il affiche les éléments corrects, ce qui signifie que l'ensemble de données n'est pas vide et contient les éléments corrects, le problème se situe donc quelque part dans la classe DiffUtil / Adapter / User


Cela semble fonctionner: les doublons n'apparaîtront plus dans la liste, mais les doublons restent dans l'ensemble de données. Existe-t-il un moyen d'empêcher l'ajout de doublons à l'ensemble de données? Parce que si l'utilisateur appelle la fonction plusieurs fois, il y aura un grand ensemble de données avec des doublons (bien qu'ils ne soient pas affichés à l'écran). Comment comparer les deux listes et les ajouter ensuite à l'ensemble de données? Parce que pour le moment, il ajoute la nouvelle liste à l'ensemble de données, et alors seulement il vérifie les doublons, j'ai donc un ensemble de données avec de nombreux doublons


J'ai une idée! Je mettrai myDataset.clear () après la fin de la boucle for , non?


Oui, vous avez raison, effacez la liste avant d'ajouter de nouveaux éléments récupérés.


Dernier problème maintenant: la liste ne se remplit à l'écran qu'au premier appel, alors elle n'ajoutera pas les éléments nouvellement ajoutés. Exemple: si au début il y a une liste de 4 éléments, alors j'appelle à nouveau la fonction et le serveur a renvoyé un autre élément donc il y en a au total 5, il ne mettra pas à jour la liste à l'écran, bien que l'ensemble de données affiche 5 éléments. La liste est bloquée dans sa configuration initiale (je dois fermer l'application, puis appeler à nouveau la fonction pour afficher les 5 éléments)


Êtes-vous sûr que votre liste de passage à updateList contient 5 éléments? Vérifiez-le s'il vous plaît avec un point de rupture.


Je suis certain que je passe à chaque fois une liste de tailles différentes, mais elle n'est bloquée que sur la première liste


Une idée de pourquoi il n'affiche que la première liste monsieur? Peut-être que cela a quelque chose à voir avec le fait que je "construis" la liste à chaque itération? (Vous voyez que j'ajoute un utilisateur à chaque pour itération de boucle)


Je l'ai vérifié à nouveau. Il semble que ça va. Parce que j'ai utilisé exactement ce modèle dans mes codes. Si vous voulez, laissez-moi voir votre code pour en connaître les aspects latents mec.



2
votes

En plus de la réponse de @ aminography, je vous suggère d'utiliser ListAdapter , une implémentation de RecyclerView.Adapter qui facilite la mise à jour de RecyclerView avec les animations appropriées. Cette classe est incluse dans la bibliothèque de support de recyclerview.

Voici un exemple d'utilisation basé sur votre cas d'utilisation:

public class MyAdapter extends ListAdapter<User, UserViewHolder> {
    public MyAdapter() {
        super(new UserDiffCallback());
    }

    public UserViewHolder onCreateViewHolder(int position, int viewType) { ... }

    public void onBindViewHolder(UserViewModel holder, int position) {
        User userAtPosition = getItem(position); // getItem is a protected method from ListAdapter
        // Bind user data to your holder...
    }
}

public class UserDiffCallback extends DiffUtil.ItemCallback<User> {

    @Override
    public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
        return oldUser.getUserId().equals(newUser.getUserId());
    }

    @Override
    public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
        // No need to check the equality for all User fields ; just check the equality for fields that change the display of your item.
        // In your case, both impact the display.
        return oldUser.getUserId().equals(newUser.getUserId()) 
                && (oldUser.getImageUrl() == null) ? newUser.getImageUrl() == null : oldUser.getImageUrl().equals(newUser.getImageUrl());
    }
}

Ensuite, lorsque vous devez mettre à jour la liste avec nouveaux utilisateurs, appelez myAdapter.submitList (newList) . Tout comme avec AsyncListDiffer , la différence entre les deux listes est calculée sur un thread d'arrière-plan.


5 commentaires

J'ai remarqué que chaque fois que j'ajoutais plus de liste à mon listadapter en utilisant submitList, la nouvelle liste remplace l'ancienne. Est-ce normal ou est-ce que je fais quelque chose de mal?


Oui, c'est un comportement normal. Lorsque vous appelez submitList , diff est calculé sur un thread d'arrière-plan puis l'ancienne liste est remplacée par la nouvelle.


Merci pour la réponse, cela signifie que ListAdapter peut réaliser un défilement sans fin car il n'ajoute pas à la liste précédente. Je me suis fait craquer la tête ... J'apprécie vraiment vos commentaires


Si je comprends bien, vous souhaitez implémenter un RecyclerView.Adapter qui charge de nouveaux éléments lorsque vous faites défiler? C'est toujours possible avec ListAdapter : fusionnez les éléments de la liste précédente avec les nouveaux, puis appelez submitList avec la liste fusionnée.


Merci, je n'y ai jamais pensé.



0
votes

On dirait
public void onBindViewHolder (@NonNull ViewHolder holder, int position, @NonNull List payloads) non implémenté Implémentez ceci pour utiliser DiffUtils correctement, car cette méthode sera appelée pour les modifications, et en fonction de la charge utile, vous pouvez mettre à jour vos éléments recyclerview au lieu d'appeler notifyDataSetChanged ()


0 commentaires

2
votes

Modifiez votre méthode:

    public void updateList(ArrayList<User> newList) {
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new MyDiffUtilCallBack(this.mDataset, newList));
        this.mDataSet.clear()
        this.mDataSet.addAll(newList)
        diffResult.dispatchUpdatesTo(this);
    }


0 commentaires