29

If we use DiffUtil.Callback, and do

adapter.setItems(itemList);
diff.dispatchUpdatesTo(adapter);

how can we make sure that adding of new elements will scroll to that new position.

I have a case where I see item disappear, and a new one is created as a first element at the top, but not visible. It is hidden on top until you scroll down to make it visible. Before using DiffUtil, I was implementing this manually, and after I knew I was inserting at some position (on top) I could scroll to.

tynn
  • 36,131
  • 8
  • 97
  • 135
miroslavign
  • 1,992
  • 2
  • 23
  • 26
  • 1
    Just add the item and use `RecyclerView.scrollToPosition(int position)`. Worked when I had to do it, even with DiffUtil. – Nicolas Apr 17 '17 at 23:29

3 Answers3

76

There's an easy way to do this that also preserves the user's scroll position if items are inserted outside the viewable area:

import android.os.Parcelable;

Parcelable recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();
// apply diff result here (dispatch updates to the adapter)
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);              

Using this approach, the new items are made visible if they are inserted where the user can see them -- but the user's viewpoint is preserved if the items are inserted outside of view.

Lorne Laliberte
  • 6,111
  • 2
  • 34
  • 36
  • 2
    Thanks! it works, just save instance state before update items and restore it right after update – Deni Erdyneev Aug 02 '18 at 09:40
  • 2
    Thanks. I use this as workaround for certain edge cases. A tracker is opened regarding unwanted auto scroll - https://issuetracker.google.com/issues/70149059 – Cheok Yan Cheng Oct 03 '18 at 18:29
  • 1
    This should be the accepted answer. Works really well! Thanks a lot! – dknchris Apr 19 '19 at 18:42
  • It seems that this is the only solution that worked out. But why? I like new ListAdapter, but sometimes it is pain in the a... Is it becasuse DiffUtil content compare is wrong? – Antonis Radz Oct 29 '19 at 14:27
23

You have a dispatchUpdatesTo(ListUpdateCallback) method to use as well.

So you could just implement a ListUpdateCallback which gives you the first element inserted

class MyCallback implements ListUpdateCallback {
    int firstInsert = -1;
    Adapter adapter = null;
    void bind(Adapter adapter) {
        this.adapter = adapter;
    }
    public void onChanged(int position, int count, Object payload) {
        adapter.notifyItemRangeChanged(position, count, payload);
    }
    public void onInserted(int position, int count) {
        if (firstInsert == -1 || firstInsert > position) {
            firstInsert = position;
        }
        adapter.notifyItemRangeInserted(position, count);
    }
    public void onMoved(int fromPosition, int toPosition) {
        adapter.notifyItemMoved(fromPosition, toPosition);
    }
    public void onRemoved(int position, int count) {
        adapter.notifyItemRangeRemoved(position, count);
    }
}

and then just scroll the RecyclerView manually

myCallback.bind(adapter)
adapter.setItems(itemList);
diff.dispatchUpdatesTo(myCallback);
recycler.smoothScrollToPosition(myCallback.firstInsert);
tynn
  • 36,131
  • 8
  • 97
  • 135
15

I used RecyclerView.AdapterDataObserver to detect when new items were added, and then scroll to the top of my list;

adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
    @Override
    public void onItemRangeInserted(int positionStart, int itemCount) {
        super.onItemRangeInserted(positionStart, itemCount);
        recycleView.smoothScrollToPosition(0);
    }
});
gregdev
  • 1,665
  • 3
  • 22
  • 28
  • 4
    This works well if you want to _always_ scroll to top when new items added. Thanks @gregdev – scottyab May 21 '19 at 11:59
  • I ended up with using this one and `onItemRangeRemoved` since I'm ok with always scrolling to top when the range is changed. `onItemRangeChanged` wasn't working correctly for me. Thanks! – Micer Sep 30 '19 at 14:25