Adding items to Endless Scroll RecyclerView with ProgressBar at bottom
Asked Answered
B

2

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?

Brave answered 6/6, 2015 at 10:48 Comment(0)
Z
84

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.

Zitella answered 7/6, 2015 at 7:20 Comment(30)
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.Brave
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.Brave
Let me know if you want me to pen another question on SO for this and I will gladly do so.Brave
@VilenMelkumyan I have tried same code, but onscrolled event is not firing to me. Any suggestions on this issue?Xray
@Xray 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.Zitella
@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?Xray
@Xray 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 barZitella
@Brave 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. :)Forearm
@Forearm 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.Brave
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 OnScrollListeners, I just called swipeRefreshLayout.setEnabled(true).Forearm
@VilenMelkumyan In my case onLoadMoreListener is getting null for 2nd time. First time its working fine. Any suggestion?Unfeigned
@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 exceptionZitella
if (onLoadMoreListener != null) { onLoadMoreListener.onLoadMore(); } this i am getting null.... Mean For 2nd time this condition goes false and does not enter if conditionUnfeigned
bcz "onLoadMoreListener" is null for me hereUnfeigned
@RishiPaul I think you are loosing reference to your adapter, by creating new one so that new adapter has no listeners attachedZitella
@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?Eads
@Eads 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.Zitella
Hi Vilen, I have a question for you regarding an onLoadMoreListener in a reverseLayout recyclerview, can you please have a look here: #34355707Brave
I followed this article : android-pratap.blogspot.com.es/2015/06/…, but when I try refresh always go top, I don't know why, can you tell me about this error ? thanksSanitize
@RishiPaul i am also getting same issue how u resolved your issue can u please help meAccomplished
@Zitella 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 ?Accomplished
@Accomplished 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 issueZitella
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.Kaluga
@Zitella 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.Roving
@Roving 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.Zitella
@VilenSorry, but you are mistaken. Look at the documentation for notifyItemRemoved, it states: Notify any registered observers that the item previously located at <code>position</code> 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 :)Roving
@Roving 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.Zitella
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.Forearm
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...Dualistic
im not able use it for GridLayoutManager. can you give some example ?Gitlow
B
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.

Brave answered 6/6, 2015 at 16:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.