20

I have implemented my RecyclerView with it's Custom Adapter as follows

Global Declarations as follows

private LinearLayoutManager linearLayoutManager;
private int pastVisibleItems, visibleItemCount, totalItemCount;
private CustomRecyclerViewAdapter customRecyclerViewAdapter;

First I created Adapter Instance inside onCreate() method which has Empty Array inside it and set it to recyclerView

linearLayoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(linearLayoutManager);
DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(
    Utility.ItemDecorationConst);
recyclerView.addItemDecoration(dividerItemDecoration);
customRecyclerViewAdapter = new CustomRecyclerViewAdapter(getActivity());

recyclerView.setAdapter(customRecyclerViewAdapter);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {

    visibleItemCount = linearLayoutManager.getChildCount();
    totalItemCount = linearLayoutManager.getItemCount();
    pastVisibleItems = linearLayoutManager.findFirstVisibleItemPosition();
    if (loading) {
        if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
            loading = false;
            customRecyclerViewAdapter.addProgressBarEntry();
            controller.getNextPage(PublisherAppContainerFragment.this);
        }
    }
}
});

After rendering complete View when I get data from AsyncTask for filling in recyclerView

I call following method of the Adapter to fill data

customRecyclerViewAdapter.addAll(myArray);

note : addAll() is not any overridden method

following is code of my CustomRecyclerViewAdapter

class CustomRecyclerViewAdapter extends RecyclerView.Adapter<CustomRecyclerViewAdapter.ViewHolder> {
    ArrayList<MyModel> arrayList = new ArrayList<>();
    Context context;

    public CustomRecyclerViewAdapter(Context context) {
        this.context = context;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder viewHolder = null;
        //inflated some view
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        //binded data to holder
    }

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

    public void addAll(ArrayList myArray) {
        this.arrayList.addAll(myArray)
    }

    public void clear() {
        arrayList.clear();
    }
    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        public CardView cardView;

        public ViewHolder(View view) {
        super(view);
            this.cardView = (CardView) view.findViewById(R.id.card_view);
            this.cardView.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
        //handle operations
        }
    }
}

So whenever I get data from AsynTask I call method addAll() and recyclerView works like charm.

Now, My question is how it's working very well even though I have never called notifyDataSetChanged() on the adapter. Are there any previously registered Observers for the adapter? who observes if the dataset which has been returned in public int getItemCount() has been changed?

As I have read from documentation

void notifyDataSetChanged ()

Notify any registered observers that the data set has changed.

that means even though there are some observers registered you need to notify them using notifyDataSetChanged(). Right?

I also called

boolean flag = customRecyclerViewAdapter.hasObservers();

to know if there are any observers registered? Flag is True.

So anyone would please help me understand how exactly these things work?

Nikhil
  • 3,711
  • 8
  • 32
  • 43
  • Paste code of your AsyncTask where you are calling addAll(). – Pravin Divraniya Sep 09 '16 at 11:24
  • I have also tested the same thing and using same adapter architecture that you have. First time its add all the items in RecyclerView. But after that addAll() will not refreshing list until I call notifyDataSetChanged or notifyItemRangeInserted. – Pravin Divraniya Sep 09 '16 at 11:27

4 Answers4

5

If you look at the source of RecyclerView setAdapter call you will find a method setAdapterInternal(adapter, false, true);which is responsible for

Replaces the current adapter with the new one and triggers listeners.

This method is responsible for swapping the old adapter with the new one and internally it also registers for the custom Data Observer. This is the reason you are getting the flag as true

Gautam
  • 3,252
  • 3
  • 23
  • 32
  • Yes it sets setAdapterInternal and also register `Observer`. I debug my application to know if it is really going into recyclerview's default `observer`. But it's not going into any `observer`. – Nikhil Sep 10 '16 at 06:25
2

Based on what I can see of your code, I would say that there are not any observers attached to your RecyclerView that are picking up changes and keeping the list updated. What is more likely is that you are just getting "lucky" as when you scroll through the list the layout manager is continually calling getItemCount() on the adapter to determine if it should show more items. Whenever you call addAll(), you silently update the item count and it just happens to appear that observers were notified of the changes.

This is definitely a bug, and you would more likely see its effects in your implementation if you were dependent on a particular observer to monitor some aspect of the list, or doing more than just appending new items to the bottom (for example altering or inserting between existing items). The correct implementation as you pointed out is to call notifyDataSetChanged() whenever the list is updated, or even better be more specific with what changed if you can. For example, you can use:

public void addAll(ArrayList myArray) {
    int positionStart = getItemCount() - 1;
    this.arrayList.addAll(myArray);
    notifyItemRangeInserted(positionStart, myArray.size());
}

public void clear() {
    int oldSize = getItemCount();
    arrayList.clear();
    notifyItemRangeRemoved(0, oldSize);
}
happydude
  • 3,869
  • 2
  • 23
  • 41
  • I really appreciate your answer. As you said _What is more likely is that you are just getting "lucky" as when you scroll through the list the layout manager is continually calling getItemCount() on the adapter_ this, might be true but at first time when I am fetching the data, I have shown `progress bar` and once I fetch data I hide the `progress bar` and make `recyclerView` visible. SO first time I am not at all scrolling but still it get loaded perfectly. How is this happening? – Nikhil Sep 05 '16 at 06:11
  • Hmm...Prior to fetching data for the first time, do you set your adapter on the RecyclerView, or do you wait to set your adapter until after you have the initial data? – happydude Sep 05 '16 at 16:58
  • I have already set my adapter with empty initialized list. And then fetch data and add to dataset of adapter but don't call `notifyDataSetChanged()` – Nikhil Sep 05 '16 at 17:01
  • Sorry, I am stumped too then. I looked through most of the RecyclerView and LinearLayoutManager code and don't see anything that stuck out to me that would cause this behavior. The reason your hasObservers() method returns true is because the RecyclerView itself sets an observer when you initially set the adapter, but it still doesn't actually seem to do anything unless you call one of the "notify" methods. – happydude Sep 05 '16 at 18:14
2

My question is how it's working very well even though I have never called notifyDataSetChanged() on the adapter

It's because the addAll method by default calls the notifyDataSetChanged().

public void addAll(T ... items) {
        synchronized (mLock) {
            if (mOriginalValues != null) {
                Collections.addAll(mOriginalValues, items);
            } else {
                Collections.addAll(mObjects, items);
            }
        }
        if (mNotifyOnChange) notifyDataSetChanged();
    }

And

public void addAll(@NonNull Collection<? extends T> collection) {
        synchronized (mLock) {
            if (mOriginalValues != null) {
                mOriginalValues.addAll(collection);
            } else {
                mObjects.addAll(collection);
            }
        }
        if (mNotifyOnChange) notifyDataSetChanged();
    }

Here's the link - https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ArrayAdapter.java

EDIT - I see that you have your own addAll method which is calling addAll method of ArrayList. This is how addAll method works -

 private ArrayList<String> ex1 = new ArrayList();
 private ArrayList<String> ex2 = new ArrayList();
 private ArrayList<String> ex3 = new ArrayList();


    ex1.add("one");
    ex2.add("two");
    ex3.addAll(ex1);
    ex3.addAll(ex2);

    System.out.println(ex3);

OUTPUT - [one, two]

This is what happening in your case.

Aniruddha
  • 4,477
  • 2
  • 21
  • 39
  • I have checked with changing `addAll()` to `AddAllItems()`. Still it's working well. So I think it has nothing to do with `addAll()` method of `ArrayAdapter` as `RecyclerVIew.Adapter` is not extended by `ArrayAdapter`. – Nikhil Sep 07 '16 at 08:44
1

I have shown progress bar and once I fetch data I hide the progress bar and make recyclerView visible - If in layout or code you set RecyclerView visibility GONE then layout will not happen and that is why Adapter.getItemsCount() not get called. So if you fetch data and populate adapter array with it and then change RecyclerView visibility from GONE to VISIBLE it will trigger update.

In case you don't call notifyDataSetChanged() RecyclerView will not know about update. I guess there is something else in your code that trigger RecyclerView update. To clarify this behavior let's use some dummy adapter:

private class DummyViewHolder extends RecyclerView.ViewHolder {
    public DummyViewHolder (View itemView) {
        super(itemView);
    }
}

private class Adapter extends RecyclerView.Adapter<DummyViewHolder> {
    private int mDummySize = 5;
    @Override
    public DummyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.dummy_view, parent, false);
        return new DummyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(DummyViewHolder holder, int position) {

    }

    void setSize(int size) { this.mDummySize = size; }

    @Override
    public int getItemCount() {
        return mDummySize;
    }
}

And in onCraete() :

    ViewHolder v = ...
    final Adapter adapter = ..
    ...
    //postpone adapter update
    (new Handler()).postDelayed(new Runnable() {
       @Override
       public void run() {
           adapter.setSize(10);//and nothing happend only 5 items on screen
       }
    }, 5000);
j2ko
  • 2,479
  • 1
  • 16
  • 29