32

I followed Vilen's excellent answer on SO: Put an indeterminate progressbar as footer in a RecyclerView grid on how to implement an endless scroll recyclerview with ProgressBar.

I implemented it myself and it works but I would like to extend the example. I want to add extra items at the top of the recyclerview, similar to how Facebook does it when you add a new status update.

I was not able to add extra items onto the list successfully - here is my code that I added onto Vilen's code in his MainActivity:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {

    int id = item.getItemId();

    if (id == R.id.add) {
        myDataset.add(0, "Newly added");
        mRecyclerView.smoothScrollToPosition(0);
        mAdapter.notifyItemInserted(0);
}
return super.onOptionsItemSelected(item);
}

When I clicked the "Add" button:

Adding a new item

When I scroll down, I get two spinners instead of one:

Scroll down

When the spinners finish and the next 5 items are loaded, the spinner is still there:

after spinner

What am I doing wrong?

Community
  • 1
  • 1
Simon
  • 18,312
  • 22
  • 130
  • 197

2 Answers2

83

The problem is that when you add new item internal EndlessRecyclerOnScrollListener doesn't know about it and counters breaking. As a matter of fact answer with EndlessRecyclerOnScrollListener has some limitations and possible problems, e.g. if you load 1 item at a time it will not work. So here is an enhanced version.

  1. Get rid of EndlessRecyclerOnScrollListener we don't need it anymore
  2. Change your adapter to this which contains scroll listener

    public class MyAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    
        private final int VIEW_ITEM = 1;
        private final int VIEW_PROG = 0;
    
        private List<T> mDataset;
    
        // The minimum amount of items to have below your current scroll position before loading more.
        private int visibleThreshold = 2;
        private int lastVisibleItem, totalItemCount;
        private boolean loading;
        private OnLoadMoreListener onLoadMoreListener;
    
        public MyAdapter(List<T> myDataSet, RecyclerView recyclerView) {
            mDataset = myDataSet;
    
            if (recyclerView.getLayoutManager() instanceof LinearLayoutManager) {
    
                final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                    @Override
                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                        super.onScrolled(recyclerView, dx, dy);
    
                        totalItemCount = linearLayoutManager.getItemCount();
                        lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition();
                        if (!loading && totalItemCount <= (lastVisibleItem + visibleThreshold)) {
                            // End has been reached
                            // Do something
                            if (onLoadMoreListener != null) {
                                onLoadMoreListener.onLoadMore();
                            }
                            loading = true;
                        }
                    }
                });
            }
        }
    
        @Override
        public int getItemViewType(int position) {
            return mDataset.get(position) != null ? VIEW_ITEM : VIEW_PROG;
        }
    
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            RecyclerView.ViewHolder vh;
            if (viewType == VIEW_ITEM) {
                View v = LayoutInflater.from(parent.getContext())
                        .inflate(android.R.layout.simple_list_item_1, parent, false);
    
                vh = new TextViewHolder(v);
            } else {
                View v = LayoutInflater.from(parent.getContext())
                        .inflate(R.layout.progress_item, parent, false);
    
                vh = new ProgressViewHolder(v);
            }
            return vh;
        }
    
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof TextViewHolder) {
                ((TextViewHolder) holder).mTextView.setText(mDataset.get(position).toString());
            } else {
                ((ProgressViewHolder) holder).progressBar.setIndeterminate(true);
            }
        }
    
        public void setLoaded() {
            loading = false;
        }
    
        @Override
        public int getItemCount() {
            return mDataset.size();
        }
    
        public void setOnLoadMoreListener(OnLoadMoreListener onLoadMoreListener) {
            this.onLoadMoreListener = onLoadMoreListener;
        }
    
        public interface OnLoadMoreListener {
            void onLoadMore();
        }
    
        public static class TextViewHolder extends RecyclerView.ViewHolder {
            public TextView mTextView;
    
            public TextViewHolder(View v) {
                super(v);
                mTextView = (TextView) v.findViewById(android.R.id.text1);
            }
        }
    
        public static class ProgressViewHolder extends RecyclerView.ViewHolder {
            public ProgressBar progressBar;
    
            public ProgressViewHolder(View v) {
                super(v);
                progressBar = (ProgressBar) v.findViewById(R.id.progressBar);
            }
        }
    }
    
  3. Change code in Activity class

    mAdapter = new MyAdapter<String>(myDataset, mRecyclerView);
    mRecyclerView.setAdapter(mAdapter);
    
    mAdapter.setOnLoadMoreListener(new MyAdapter.OnLoadMoreListener() {
        @Override
        public void onLoadMore() {
            //add progress item
            myDataset.add(null);
            mAdapter.notifyItemInserted(myDataset.size() - 1);
    
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    //remove progress item
                    myDataset.remove(myDataset.size() - 1);
                    mAdapter.notifyItemRemoved(myDataset.size());
                    //add items one by one
                    for (int i = 0; i < 15; i++) {
                        myDataset.add("Item" + (myDataset.size() + 1));
                        mAdapter.notifyItemInserted(myDataset.size());
                    }
                    mAdapter.setLoaded();
                    //or you can add all at once but do not forget to call mAdapter.notifyDataSetChanged();
                }
            }, 2000);
            System.out.println("load");
        }
    });
    

The rest remains unchanged, let me know if this works for you.

Austyn Mahoney
  • 11,078
  • 7
  • 59
  • 85
Vilen
  • 4,973
  • 3
  • 24
  • 38
  • 1
    Thanks Vilen. While my answer works, i have noticed that if i add more than 1 item onto my list, the spinner appears twice and does not work as smoothly as your answer. I have accepted your answer as the correct answer. +1. – Simon Jun 07 '15 at 10:24
  • 1
    Vilen, I have implemented an endless scroll for my recyclerview but once it gets to the end all the items in my recyclerview, I have noticed that the OnScrollListener actually gets called twice as 1. Its gets called when I scroll down to activate it 2. It gets called again when I add nothing to my recyclerview as totalItemCount = (lastVisibleItem + visibleThreshold) as I have reached the end of my list. Is there anything I can do to counter it? I think the logic of the loadMore adapter (!loading && totalItemCount <= (lastVisibleItem + visibleThreshold)) should change but not sure. – Simon Jun 27 '15 at 17:47
  • Let me know if you want me to pen another question on SO for this and I will gladly do so. – Simon Jun 27 '15 at 18:26
  • @VilenMelkumyan I have tried same code, but onscrolled event is not firing to me. Any suggestions on this issue? – Beginner Sep 14 '15 at 10:47
  • @Beginner well it is hard to guess without seeing your code, suggestion is set breakpoints to locate where code doesn't reach. for example it might happen that your layout manager is not type of LinearLayoutManager or your adapter is never instantiated, or somewhere else you set another scroll listener which cancels existing one. – Vilen Sep 14 '15 at 11:16
  • @VilenMelkumyan actually i'm using GridLayoutManager and i'm checking for same. Above code will not work for GridLayoutManager, when LinearLayoutManager are modified to GridLayoutManager? – Beginner Sep 14 '15 at 11:38
  • @Beginner it should but you need to modify it properly and in all places it might be a little tricky to create entire row for progress bar – Vilen Sep 14 '15 at 11:50
  • @Simon if know that your answer has this bug then it's better to either add a note to the answer stating the issue or remove the answer altogether. :) – Sufian Oct 22 '15 at 06:32
  • @Sufian its actually not a bug. My answer works as well, its just the way the data was represented. If you had replaced the data in my answer with something other than numbers, then you will see that all the data would be represented. – Simon Oct 22 '15 at 07:56
  • 1
    Awesome answer. To make it work with `swipeRefreshLayout`, I just need to add `swipeRefreshLayout.setEnabled(layoutManager.findFirstCompletelyVisibleItemPosition() == 0);` in `OnScrollListener().onScrolled()`. And when removing the `OnScrollListener`s, I just called `swipeRefreshLayout.setEnabled(true)`. – Sufian Oct 22 '15 at 14:22
  • @VilenMelkumyan In my case onLoadMoreListener is getting null for 2nd time. First time its working fine. Any suggestion? – Rishi Paul Nov 02 '15 at 07:56
  • @RishiPaul I am not sure what do you mean by "getting null", what object object is null, or on what object you get null pointer exception – Vilen Nov 02 '15 at 09:01
  • if (onLoadMoreListener != null) { onLoadMoreListener.onLoadMore(); } this i am getting null.... Mean For 2nd time this condition goes false and does not enter if condition – Rishi Paul Nov 02 '15 at 09:05
  • bcz "onLoadMoreListener" is null for me here – Rishi Paul Nov 02 '15 at 09:14
  • @RishiPaul I think you are loosing reference to your adapter, by creating new one so that new adapter has no listeners attached – Vilen Nov 02 '15 at 11:39
  • @VilenMelkumyan will your example also maintain the scroll position? Let's say the "newly added" item is added asynchronously while the user is in the middle of the list. If not, is it possible to retain the scroll position? – Rihards Nov 27 '15 at 15:31
  • @Rihards what you mentioned is a different thing and should be handled differently, if you want to add items while I'm in the middle of the list you should update scroll position programmatically, i.e. After dataset is updated and notifyDatasetChanged is called you also need to call set scroll position on your layout manager. – Vilen Nov 27 '15 at 18:11
  • Hi Vilen, I have a question for you regarding an onLoadMoreListener in a reverseLayout recyclerview, can you please have a look here: http://stackoverflow.com/questions/34355207/adding-items-to-endless-scroll-recyclerview-with-a-reverselayout-recyclerview – Simon Dec 18 '15 at 12:00
  • I followed this article : http://android-pratap.blogspot.com.es/2015/06/endless-recyclerview-with-progress-bar.html, but when I try refresh always go top, I don't know why, can you tell me about this error ? thanks –  Mar 07 '16 at 10:08
  • @RishiPaul i am also getting same issue how u resolved your issue can u please help me – Erum May 11 '16 at 15:55
  • @Vilen i followed your answer but i am getting issue i have to add 10 items every time on first time 10 items then when user reaches till the 10th item then it should download 10 more items and append in list now while my TotalRecords length is 50 so it should show total records in 5th time scroll but this is not happening any reason ? – Erum May 13 '16 at 21:29
  • @Erum I am not sure what goes wrong on your side, above solution was tested with way more items. perhaps you have a small bug in your code, please make sure you don't have changes that may result in unexpected behavior, use breakpoints to debug the issue – Vilen May 15 '16 at 06:40
  • It is wrong code! java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling. This error when adapter contain only one item. – GPPSoft May 18 '16 at 06:17
  • @Vilen As far as I understand there is a flaw in your code. When you call **myDataset.remove(myDataset.size() - 1);** you need to call **mAdapter.notifyItemRemoved(myDataset.size() - 1);** but you call **mAdapter.notifyItemRemoved(myDataset.size());** . My implementation of your code gave an **IndexOutOfBoundsException: Inconsistency detected** time after time. When I fixed it there was no such exception yet. – zkvarz Sep 21 '16 at 08:30
  • @zkvarz thanks for observation, if it crashes then it might be a case but logically it shouldn't because notifyItemRemoved notifies adapter to update view based on previous dataset, e.g. if there was 10 items in adapter and last position in your dataset is 9 (because counting starts from 0) after you remove item by calling myDataset.remove(size-1) you have 9 items in your dataset and size becomes equal to 9 but in view there is still 10 so notifyItemRemoved(9) still points to last view item. – Vilen Oct 18 '16 at 06:10
  • @VilenSorry, but you are mistaken. Look at the documentation for notifyItemRemoved, it states: **Notify any registered observers that the item previously located at position has been removed from the data set.**. Which means when you call **adapter.remove(position)** you should call **adapter.notifyItemRemoved(position)** to actually notify that item is removed :) – zkvarz Oct 19 '16 at 08:07
  • @zkvarz I am not sure because as you mentioned it says "previously" which I understand as previous position of the item which was removed. still I am not sure I might be wrong but if you changed it to size-1 and it worked then that's good. – Vilen Oct 19 '16 at 09:40
  • FWIW, putting `swipeRefreshLayout.setEnabled(false)` (as I had suggested in my earlier comment) will hide the "refresh" animation. Instead I call `swipeRefreshLayout.setRefreshing(false)` as soon as I call `onLoadMoreListener.onLoadMore();` (refresh animation will go away only when `loadMore()` is called. – Sufian Oct 28 '16 at 11:26
  • It would be wiser to use `mAdapter.getItemCount()` instead of `myDataset.size()` to cater for extra elements within the adapter like section headers. Awesome answer btw... – muruthi Nov 29 '16 at 12:06
  • im not able use it for GridLayoutManager. can you give some example ? – Ashok Reddy M May 07 '18 at 05:46
2

I think I figured it out.

I forgot to call notifyItemRangeChanged.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.add) {
        myDataset.add(0, "Newly added");
        mAdapter.notifyItemInserted(0);
        mAdapter.notifyItemRangeChanged(1, myDataset.size());
        mRecyclerView.smoothScrollToPosition(0);
}
return super.onOptionsItemSelected(item);
}

Once you add it, the code will work, however, you will see that after the spinner finishes spinning, the item number will not increment properly.

increment

This is because the "Newly added" item on top counts as an actual item (we can call it "Item 0"), and this cause the increment to shift by 1 like 21 has been skipped, but actually number 21 has become Item 0. In other words, there are 21 actual items before Item 22.

Simon
  • 18,312
  • 22
  • 130
  • 197