7

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);
aminography
  • 21,986
  • 13
  • 70
  • 74
pileup
  • 1
  • 2
  • 18
  • 45
  • 1
    It seems to be correct. Please post the definition of `mAdapter`? – aminography Jan 12 '19 at 18:34
  • added. By the way, the fetching data is inside a method that's called by an onClick event, I will edit post to explain. From some reason the `updateList` is not visible on the mAdapter :/ – pileup Jan 12 '19 at 18:41
  • 1
    Have you redefined `equals` for your `User` class ? Without it, `areContentsTheSame` will not work properly. – Thibault Seisel Jan 12 '19 at 18:47
  • You're right, nothing is displayed, you think it's because the equals? Can you please be more specific? I am new to Android so I don't know how to redefine it – pileup Jan 12 '19 at 18:55
  • @ThibaultSeisel can you please add how do I need to change it? I can't make it work – pileup Jan 12 '19 at 21:38

4 Answers4

7

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

}
aminography
  • 21,986
  • 13
  • 70
  • 74
  • Thank you, now 1 problem is solved: the `updateList` is visible, but now the list won't show anything (nothing shows up although I get data). When I test it with `notifyDataSetChanged` it does display the data – pileup Jan 12 '19 at 18:55
  • 1
    You're welcome. I think it is better to define `public MyAdapter()` instead of `public MyAdapter(ArrayList myDataset)` and in `MyAdapter` define and instantiate `mDataset` as `private ArrayList mDataset = new ArrayList();`. It may solve the problem. – aminography Jan 12 '19 at 19:17
  • you mean leave the `public MyAdapter() {}` empty? And what about the other comment? About overriding `equals` method in my `User` class? I did what you said, and it still displays nothing :( Also note that there are `mDataset` and `myDataset` not to be confused with both – pileup Jan 12 '19 at 19:37
  • Could you please edit your answer with possible fixes so I see exactly what to do? Thanks – pileup Jan 12 '19 at 19:45
  • 1
    Yes, I mean it do not pass list of user to it. It leads to empty `mDataset` at the first time, then when a list has retrieved from the database, `DiffUtil` calculates diff between an empty list and retrieved list that leads to show all of new data. Sure, I'll edit it. – aminography Jan 12 '19 at 20:13
  • 1
    Ok, I did exactly that, but nothing shows up, we're missing something. Someone in the comments mentioned something about overriding the `equals` inside the `User` class, which I haven't done, and I don't know how to do it. But I'm not sure if that's going to fix it. What are we missing? `updateList` is supposed to replace `notifyDataSetChanged` completely? – pileup Jan 12 '19 at 20:36
  • 1
    Yes, there is a bug in `areItemsTheSame`. As you defined `mUserId` as `String`, you should check the equality using `equals`, not `==`. So we have: `return oldUsers.get(oldItemPosition).getUserId().equals(newUsers.get(newItemPosition).getUserId());` I edit the answer with updated `User` class. It may fix the problem as you guess. – aminography Jan 12 '19 at 20:49
  • 1
    Updated `updateList` does not equal to `notifyDataSetChanged`. Because the heavy part in updating the whole items is to reassign all values to the views. It just replaces the data `ArrayList` and using `DiffUtil` it updates the only required `View`s. – aminography Jan 12 '19 at 20:57
  • Actually I already changed this part to `equals`, but he mentioned something else, overriding equals in `areContentsTheSame`. Still nothing shows up. maybe I must call at least once `onDataSetChanged` at first? – pileup Jan 12 '19 at 21:04
  • 1
    `onDataSetChanged` does not needed at all. Is it possible to share more code of the fragment, or better to share the project in a private github repo and add me to it. – aminography Jan 12 '19 at 21:11
  • The project is a mess, because I'm a beginner, there really is not much to add. By the way, when I log `myDataset` on each iteration it shows the correct items, meaning the dataset is not empty and has the correct items, so the problem is somewhere in the DiffUtil / Adapter / User class – pileup Jan 12 '19 at 21:36
  • It seems to be working: Now duplicates won't show on the list, but the duplicates remain in the dataset. Is there a way to prevent duplicates from being added to the dataset? Because if the user calls the function many times, there will be a large dataset with duplicates (Although they won't be shown on the screen). How to compare the two list, and only then add them to the dataset? Because right now it adds the new list to the dataset, and only then it checks for duplicates, so I have dataset with many duplicates – pileup Jan 13 '19 at 11:49
  • I have an idea! I will put `myDataset.clear()` after the end of the `for` loop, right? – pileup Jan 13 '19 at 12:17
  • Yes, you are right, clear the list before adding new retrieved items. – aminography Jan 13 '19 at 12:21
  • Last problem now: The list populates on the screen only on the first call, then it will not add newly added items. Example: If at first there is a list of 4 items, then I call the function again and the server returned another item so there are total of 5, it will not update the list on the screen, although the dataset does show 5 items. The list is stuck in its initial setup(I have to close the app, and then call the function again to display the 5 items) – pileup Jan 13 '19 at 18:13
  • 1
    Do you sure your passing list to `updateList` is 5 items? Check it please with a break point. – aminography Jan 14 '19 at 05:45
  • I am certain I pass everytime different size list, but it's stuck only on the first list – pileup Jan 14 '19 at 13:21
  • Any idea why it's only displaying the first list sir? Maybe it has something to do with the fact that I "build" the list on each iteration? (You see I add user on every `for` loop iteration) – pileup Jan 14 '19 at 19:38
  • I've checked it again. It seems that it is ok. Because I used exactly this pattern in my codes. If you want, let me see your code to know latent aspects of it dude. – aminography Jan 14 '19 at 19:58
4

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.

Thibault Seisel
  • 1,197
  • 11
  • 23
  • I noticed whenever I added more list to my listadapter using submitList the new list overrides the old one. Is this normal or am I doing something wrong? – Prodigy May 23 '19 at 23:01
  • Yes, that's normal behavior. When you call `submitList`, diff is calculated on a background thread then the old list is replaced with the new one. – Thibault Seisel May 24 '19 at 18:50
  • Thanks for the response, that means ListAdapter can achieve unending scrolling since it doesn't add to the previous list. I've been cracking up my heads....Really appreciate your feedback – Prodigy May 24 '19 at 21:16
  • 1
    If I understand well, you want to implement a `RecyclerView.Adapter` that loads new items as you scroll ? It's still possible with `ListAdapter` : merge the items from the previous list with the new ones, then call `submitList` with the merged list. – Thibault Seisel May 26 '19 at 14:42
  • Thanks, I never thought of that. – Prodigy May 26 '19 at 20:10
4

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);
    }
Ayush Jain
  • 563
  • 7
  • 11
0

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

Coder
  • 21
  • 2
  • 4