2

I have a simple recyclerview with items (tips) and a loading spinner at the bottom.

here's how the item count and item view type methods look:

  @Override
  public int getItemViewType(int position) {
    if (position == getItemCount() - 1) { // last position
      return LOADING_FOOTER_VIEW_TYPE;
    }
    else {
      return TIP_VIEW_TYPE;
    }
  }

  @Override
  public int getItemCount() {
    return tips.size() + 1; // + 1 for the loading footer
  }

basically, i just have a loading spinner under all my items.

I create the adapter once like so:

  public TipsListAdapter(TipsActivity tipsActivity, ArrayList<Tip> tips) {
    this.tipsActivity = tipsActivity;
    this.tips = tips;
  }

and then once i have fetched additional items, i call add like so:

public void addTips(List<Tip> tips) {

    // hide the loading footer temporarily
    isAdding = true;
    notifyItemChanged(getItemCount() - 1);

    // insert the new items
    int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
    this.tips.addAll(tips);
    notifyItemRangeInserted(insertPos, tips.size());


    // allow the loading footer to be shown again
    isAdding = false;
    notifyItemChanged(getItemCount() - 1);

  }

What's odd here is that when i do that, the scroll position goes to the very bottom. It almost seems like it followed the loading spinner. This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).

This doesn't happen if i change notifyItemRangeInserted() to notifyItemRangeChanged() like so:

public void addTips(List<Tip> tips) {

    // hide the loading footer temporarily
    isAdding = true;
    notifyItemChanged(getItemCount() - 1);

    // insert the new items
    int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
    this.tips.addAll(tips);
    notifyItemRangeChanged(insertPos, tips.size());


    // allow the loading footer to be shown again
    isAdding = false;
    notifyItemChanged(getItemCount() - 1);

  }

Nor does it happen if i simply call notifyDataSetChanged() like so:

  public void addTips(List<Tip> tips) {

    this.tips.addAll(tips);
    notifyDataSetChanged();

  }

Here's the code for setting the adapter in my Activity:

  public void setAdapter(@NonNull ArrayList<Tip> tips) {
    if (!tips.isEmpty()) { // won't be empty if restoring state
      hideProgressBar();
    }

    tipsList.setAdapter(new TipsListAdapter(this, tips));
  }

 public void addTips(List<Tip> tips) {
    hideProgressBar();
    getAdapter().addTips(tips);
    restorePageIfNecessary();
  }

  private TipsListAdapter getAdapter() {
    return (TipsListAdapter) tipsList.getAdapter();
  }

Note:

  • I don't manually set scroll position anywhere.
  • I call setAdapter() in onResume()
  • addTips() is called after I fetch items from the server

Let me know if you need any additional parts of my code.

Sree
  • 2,727
  • 3
  • 29
  • 47

2 Answers2

9

This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).

RecyclerView has built-in behavior when calling the more-specific dataset change methods (like notifyItemRangeInserted() as opposed to notifyDataSetChanged()) that tries to keep the user looking at "the same thing" as before the operation.

When the data set changes, the first item the user can see is prioritized as the "anchor" to keep the user looking at approximately the same thing. If possible, the RecyclerView will try to keep this "anchor" view visible after the adapter update.

On the very first load, the first item (the only item) is the loading indicator. Therefore, when you load the new tips and update the adapter, this behavior will prioritize keeping the loading indicator on-screen. Since the loading indicator is kept at the end of the list, this will scroll the list to the bottom.

On subsequent loads, the first item is not the loading indicator, and it doesn't move. So the RecyclerView will not appear to scroll, since it doesn't have to do so to keep the "anchor" on-screen.

My recommendation is to check insertPos and see if it is zero. If it is, that means this is the first load, so you should update the adapter by calling notifyDataSetChanged() in order to avoid this anchoring behavior. Otherwise, call notifyItemRangeInserted() as you're currently doing.

Ben P.
  • 52,661
  • 6
  • 95
  • 123
  • ah, that makes sense. i am assuming that `notifyItemRangeChanged` works the same way as `notifyDataSetChanged`? – Sree Feb 27 '18 at 20:24
  • @Sree not exactly. `notifyItemRangeChanged()` will tell the adapter that the items in the range have changed and should be re-bound, but items outside the range have not been changed and don't need to be re-bound. `notifyDataSetChanged()` will tell the adapter that the entire set of items has changed, and everything should be re-bound. Considering that you're adding items to the dataset, not re-binding items outside the range could be problematic. – Ben P. Feb 27 '18 at 20:26
  • ty...that enriched my knowledge too .. xD – Santanu Sur Feb 27 '18 at 20:27
  • @BenP. ah ok, so do you know why `notifyItemRangeChanged()` was working for me? i had actually initially assumed the same thing as what you said, but was surprised when it ended up working. – Sree Feb 27 '18 at 20:28
  • @Sree meant that she assumed that` both` worked the same way before you explained – Santanu Sur Feb 27 '18 at 20:29
  • @Sree it comes down to how the data set has changed. You're just adding items to the end of the list, which doesn't actually affect anything else in the list (except for the loading indicator, which you're also refreshing). It would only be an issue if the insertion of items affected the other items but they didn't get re-bound. – Ben P. Feb 27 '18 at 20:30
0

Remove the setAdapter code from onResume ASAP as you are setting new TipsListAdapter(this, tips);

Every time a new reference of the adapter is created...make field mAdapter and then set it in onCreate . RecyclerView doesnt remember the scrolled position because everytime a new reference of adapter is being created.. onResume gets called infinitely when activity is in running state..

So either you setAdapter in onCreate using new operator to create reference for adapter or,

in onResume use mAdapter field variable reference..

Santanu Sur
  • 10,997
  • 7
  • 33
  • 52
  • That didn't work. Setting it in `onResume()` won't matter because it is only called once per state change. Secondly, that also wouldn't be the reason why `notifyItemRangeChanged` and `notifyDataSetChanged` doesn't reproduce the issue. And also check out how I am getting the adapter -- made an edit in my original question. I am always referencing the same adapter. – Sree Feb 27 '18 at 20:15