Refreshing data in RecyclerView and keeping its scroll position

How does one refresh the data displayed in RecyclerView (calling notifyDataSetChanged on its adapter) and make sure that the scroll position is reset to exactly where it was?

In case of good ol' ListView all it takes is retrieving getChildAt(0), checking its getTop() and calling setSelectionFromTop with the same exact data afterwards.

It doesn't seem to be possible in case of RecyclerView.

I guess I'm supposed to use its LayoutManager which indeed provides scrollToPositionWithOffset(int position, int offset), but what's the proper way to retrieve the position and the offset?

layoutManager.findFirstVisibleItemPosition() and layoutManager.getChildAt(0).getTop()?

Or is there a more elegant way to get the job done?


Solution 1:

I use this one.^_^

// Save state
private Parcelable recyclerViewState;
recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();

// Restore state
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);

It is simpler, hope it will help you!

Solution 2:

I have quite similar problem. And I came up with following solution.

Using notifyDataSetChanged is a bad idea. You should be more specific, then RecyclerView will save scroll state for you.

For example, if you only need to refresh, or in other words, you want each view to be rebinded, just do this:

adapter.notifyItemRangeChanged(0, adapter.getItemCount());

Solution 3:

EDIT: To restore the exact same apparent position, as in, make it look exactly like it did, we need to do something a bit different (See below how to restore the exact scrollY value):

Save the position and offset like this:

LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
int firstItem = manager.findFirstVisibleItemPosition();
View firstItemView = manager.findViewByPosition(firstItem);
float topOffset = firstItemView.getTop();

outState.putInt(ARGS_SCROLL_POS, firstItem);
outState.putFloat(ARGS_SCROLL_OFFSET, topOffset);

And then restore the scroll like this:

LinearLayoutManager manager = (LinearLayoutManager) mRecycler.getLayoutManager();
manager.scrollToPositionWithOffset(mStatePos, (int) mStateOffset);

This restores the list to its exact apparent position. Apparent because it will look the same to the user, but it will not have the same scrollY value (because of possible differences in landscape/portrait layout dimensions).

Note that this only works with LinearLayoutManager.

--- Below how to restore the exact scrollY, which will likely make the list look different ---

  1. Apply an OnScrollListener like so:

    private int mScrollY;
    private RecyclerView.OnScrollListener mTotalScrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            mScrollY += dy;
        }
    };
    

This will store the exact scroll position at all times in mScrollY.

  1. Store this variable in your Bundle, and restore it in state restoration to a different variable, we'll call it mStateScrollY.

  2. After state restoration and after your RecyclerView has reset all its data reset the scroll with this:

    mRecyclerView.scrollBy(0, mStateScrollY);
    

That's it.

Beware, that you restore the scroll to a different variable, this is important, because the OnScrollListener will be called with .scrollBy() and subsequently will set mScrollY to the value stored in mStateScrollY. If you do not do this mScrollY will have double the scroll value (because the OnScrollListener works with deltas, not absolute scrolls).

State saving in activities can be achieved like this:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt(ARGS_SCROLL_Y, mScrollY);
}

And to restore call this in your onCreate():

if(savedState != null){
    mStateScrollY = savedState.getInt(ARGS_SCROLL_Y, 0);
}

State saving in fragments works in a similar way, but the actual state saving needs a bit of extra work, but there are plenty of articles dealing with that, so you shouldn't have a problem finding out how, the principles of saving the scrollY and restoring it remain the same.