15

I am trying to create messaging kind of screen using recyclerView which will start from bottom and will loadMore data when user reached top end of chat. But I am facing this weird issue.

My recyclerView scrolls to top on calling notifyDataSetChanged. Due to this onLoadMore gets called multiple times.

Here is my code:

LinearLayoutManager llm = new LinearLayoutManager(context);
llm.setOrientation(LinearLayoutManager.VERTICAL);
llm.setStackFromEnd(true);
recyclerView.setLayoutManager(llm);

** In Adapter

 @Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (messages.size() > 8 && position == 0 && null != mLoadMoreCallbacks) {
        mLoadMoreCallbacks.onLoadMore();
    }

** In Activity

@Override
public void onLoadMore() {
    // Get data from database and add into arrayList
      chatMessagesAdapter.notifyDataSetChanged();
}

It's just that recyclerView scrolls to top. If scrolling to top stops, this issue will be resolved. Please help me to figure out the cause of this issue. Thanks in advance.

user1288005
  • 740
  • 2
  • 11
  • 33
  • you can try adding mLayoutManager.setReverseLayout(true); to your layoutmanager. – Muhib Pirani Apr 15 '17 at 12:50
  • set your adapter with notify again. may be it will help you – parik dhakan Apr 18 '17 at 10:54
  • addonscrollListener to your recyclerview and find proper position of your current visible item then call onLoadMore() method, and try to call this method from your activity rather then adapter – Bhavnik Apr 18 '17 at 12:08

10 Answers10

5

I think you shouldn't use onBindViewHolder that way, remove that code, the adapter should only bind model data, not listen scrolling.

I usually do the "onLoadMore" this way:

In the Activity:

private boolean isLoading, totallyLoaded; //
RecyclerView mMessages;
LinearLayoutManager manager;
ArrayList<Message> messagesArray;
MessagesAdapter adapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //...
    mMessages.setHasFixedSize(true);
    manager = new LinearLayoutManager(this);
    manager.setStackFromEnd(true);
    mMessages.setLayoutManager(manager);
    mMessages.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            if (manager.findFirstVisibleItemPosition() == 0 && !isLoading && !totallyLoaded) {
                onLoadMore();
                isLoading = true;
            }
        }
    });
    messagesArray = new ArrayList<>();
    adapter = new MessagesAdapter(messagesArray, this);
    mMessages.setAdapter(adapter);
}


@Override
public void onLoadMore() {
    //get more messages...
    messagesArray.addAll(0, moreMessagesArray);
    adapter.notifyItemRangeInserted(0, (int) moreMessagesArray.size();
    isLoading = false;
}

This works perfeclty for me, and the "totallyLoaded" is used if the server doesn't return more messages, to stop making server calls. Hope it helps you.

Borja
  • 1,190
  • 1
  • 13
  • 26
2

You see, it's natural for List to scroll to the most top item when you insert new Items. Well you going in the right direction but I think you forgot adding setReverseLayout(true).

Here the setStackFromEnd(true) just tells List to stack items starting from bottom of the view but when used in combination with the setReverseLayout(true) it will reverse order of items and views so the newest item is always shown at the bottom of the view.

Your final layoutManager would seems something like this:

        mLayoutManager = new LinearLayoutManager(getActivity());
        mLayoutManager.setReverseLayout(true);
        mLayoutManager.setStackFromEnd(true);
        mRecyclerView.setLayoutManager(mLayoutManager);
Keivan Esbati
  • 2,855
  • 1
  • 17
  • 35
1

This is my way to avoid scrollview move to top Instead of using notifyDataSetChanged(), I use notifyItemRangeChanged();

List<Object> tempList = new ArrayList<>();
tempList.addAll(mList);
mList.clear();
mList.addAll(tempList);
notifyItemRangeChanged(0, mList.size());

Update: For another reason, Your another view in the top is focusing so it will jump to top when you call any notifies, so remove all focuses by adding android:focusableInTouchMode="true" in the GroupView.

grrigore
  • 1,080
  • 1
  • 17
  • 29
0

I do not rely on onBindViewHolder for these kind of things. It can be called multiple times for a position. For the lists which has load more option maybe you should use something like this after your recyclerview inflated.

    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if ((((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition() == 0)) {
                if (args.listModel.hasMore && null != mLoadMoreCallback && !loadMoreStarted) {
                    mLoadMoreCallbacks.onLoadMore();
                }
            }
        }
    });

Hope it helps.

  • My loadMore functionality is working fine. Loadmore function get called a lot of times because my recyclerView scrolls to top after notifyDatasetChanged. If it doesn't scrolls to top it should work fine. – user1288005 Mar 30 '17 at 17:36
  • Does your recyclerview have wrap_content property? If it does this might happen for measuring purposes. If it's not that maybe using notifyItemChanged or notifyItemInserted will be the solution. By the way with my solution above I am too calling notifyDatasetChanged for a normal (start from top) list but it does not scroll to top after calling. It ads more items under last visible item. – letitthebee Mar 30 '17 at 17:59
0

I suggest you to use notifyItemRangeInserted method of RecyclerView.Adapter for LoadMore operations. You add a set of new items to your list so you do not need to notify whole dataset.

notifyItemRangeInserted(int positionStart, int itemCount)

Notify any registered observers that the currently reflected itemCount items starting at positionStart have been newly inserted.

For more information: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html

savepopulation
  • 10,674
  • 4
  • 47
  • 68
  • 1
    I already tried this one and it works as I wanted but sometimes it gives me this error: Non-fatal Exception: java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 13(offset:13).state:21 at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4405) at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4363) – user1288005 Apr 12 '17 at 08:04
  • i think you're sending first parameter (positionStart) wrong so you get this error. if you have 10 items in your list before load more and you loaded 10 more items your positionStart is 10 and itemCount is 10. – savepopulation Apr 12 '17 at 08:09
  • 1
    ıf it's worked for you, i'll be glad if you accept my answer and upvote. thanks. – savepopulation Apr 12 '17 at 08:18
0

DON'T call notifyDataSetChanged() on the RecyclerView. Use the new methods like notifyItemChanged(), notifyItemRangeChanged(), notifyItemInserted(), etc... And if u use notifyItemRangeInserted()--

don't call setAdapter() method after that..!

Shobhit
  • 978
  • 9
  • 29
0

You have this issue because every time your condition be true you call loadMore method even loadMore was in running state, for solving this issue you must put one boolean value in your code and check that too.

check my following code to get more clear.

1- declare one boolean value in your adapter class

2- set it to true in your condition

3- set it to false after you've got data from database and notified your adapter.

so your code must be like as following code:

public class YourAdapter extend RecylerView.Adapter<.....> {

   private boolean loadingDataInProgress = false;


   public void setLoadingDataInProgress(boolean loadingDataInProgress) {
       this.loadingDataInProgress = loadingDataInProgress
   }


   .... 
   // other code


   @Override
   public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
      if (messages.size() > 8 && position == 0 && null != mLoadMoreCallbacks && !loadingDataInProgress){
         loadingDataInProgress = true;
         mLoadMoreCallbacks.onLoadMore();
    }


   ......
   //// other adapter code

}

in Activity :

@Override
public void onLoadMore() {
      // Get data from database and add into arrayList
      chatMessagesAdapter.notifyDataSetChanged();

      chatMessagesAdapter. setLoadingDataInProgress(false);
}

This must fix your problem but I prefer to handle loadMore inside Activity or Presenter class with set addOnScrollListener on RecyclerView and check if findFirstVisibleItemPosition in LayoutManager is 0 then load data.

I've wrote one library for pagination, feel free to use or custom it.

PS: As other user mentioned don't use notifyDataSetChanged because this will refresh all view include visible views that you don't want to refresh those, instead use notifyItemRangeInsert, in your case you must notify from 0 to size of loaded data from database. In your case as you load from top, notifyDataSetChanged will change scroll position to top of new loaded data, so you MUST use notifyItemRangeInsert to get good feel in your app

Shayan Pourvatan
  • 11,622
  • 4
  • 39
  • 62
0

You need to nofity the item in specific range like below:

       @Override
        public void onLoadMore() {
            // Get data from database and add into arrayList
            List<Messages> messegaes=getFromDB();
            chatMessagesAdapter.setMessageItemList(messages);
            // Notify adapter with appropriate notify methods
            int curSize = chatMessagesAdapter.getItemCount();
            chatMessagesAdapter.notifyItemRangeInserted(curSize,messages.size());

        }
Burhanuddin Rashid
  • 4,736
  • 5
  • 32
  • 48
0

Checkout Firebase Friendlychat source-code on Github.

It behaves like you want, specially at:

    mFirebaseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            int friendlyMessageCount = mFirebaseAdapter.getItemCount();
            int lastVisiblePosition = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
            // If the recycler view is initially being loaded or the user is at the bottom of the list, scroll
            // to the bottom of the list to show the newly added message.
            if (lastVisiblePosition == -1 ||
                    (positionStart >= (friendlyMessageCount - 1) && lastVisiblePosition == (positionStart - 1))) {
                mMessageRecyclerView.scrollToPosition(positionStart);
            }
        }
    });
gustavogbc
  • 705
  • 10
  • 30
0

You need to nofity the item in specific range

   @Override
    public void onLoadMore() {
        // Get data from database and add into arrayList
        List<Messages> messegaes=getFromDB();
        chatMessagesAdapter.setMessageItemList(messages);
        // Notify adapter with appropriate notify methods
        int curSize = chatMessagesAdapter.getItemCount();
        chatMessagesAdapter.notifyItemRangeInserted(curSize,messages.size());

    }
Harsh Singhal
  • 547
  • 4
  • 12