pileup
pileup

Reputation: 3292

What's wrong with my DiffUtil implementation?

Update: One of the problems is solved: Now updateList is resolved, the problem was that I defined mAdapter as RecyclerView.Adapter instead of MyAdapter. But now even though I am getting data, nothing shows up on the list, it's empty

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

I want to update my RecyclerView using DiffUtil to prevent duplicates.

I have 4 classes: The User class, the Activity class where I set data, the Adapter class and the DiffUtil class. I am not sure I combine all these 4 correctly.

This is the User class:

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

This is how I set data dynamically (I keep getting new Json arrays from the server containing user id's to be displayed, then I set the user image from Firebase storage): (It's a function invoked by an onClick listener:)

This is the method call from the fragment:

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

This is the function:

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

This is how my DiffUtil class looks like:

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

And this is my adapter:

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

I am not sure I combine all the classes correctly (my first time using DiffUtil), and I also get cannot resolve method updateList(?)

What am I doing wrong?

This is how I define mAdapter in my Fragment:

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

Upvotes: 7

Views: 12796

Answers (4)

aminography
aminography

Reputation: 22832

The problem comes from definition of mAdapter. You defined it as RecyclerView.Adapter which is super class of your MyAdapter and it does not contain updateList(). You should change it as following:

private MyAdapter mAdapter;

Updated 1/13/2019:

I've revised your adapter with AsyncListDiffer which calculates the diffrence asynchronously then applies it to the adapter.

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

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

}

Upvotes: 7

Ayush Jain
Ayush Jain

Reputation: 583

Modify your method:

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

Upvotes: 4

Coder
Coder

Reputation: 21

Looks like
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) not implemented Implement this to use DiffUtils properly, as this method will be called for the changes, and based on the payload you can update your recyclerview items instead of calling notifyDataSetChanged()

Upvotes: 0

Thibault Seisel
Thibault Seisel

Reputation: 1257

In addition to @aminography's answer, I suggest you to use ListAdapter, a RecyclerView.Adapter implementation that makes it easier to update you RecyclerView with the correct animations. This class is included in the recyclerview support library.

Below is an example of usage based on your use case:

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

Then, when you need to update the list with new users, call myAdapter.submitList(newList). Juste like with AsyncListDiffer, the diff between the two list is calculated on a background Thread.

Upvotes: 4

Related Questions