Refreshing data in RecyclerView and keeping its scroll position

AndroidAndroid Recyclerview

Android Problem Overview


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?

Android Solutions


Solution 1 - Android

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 - Android

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 - Android

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.

Solution 4 - Android

Keep scroll position by using @DawnYu answer to wrap notifyDataSetChanged() like this:

val recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState() 
adapter.notifyDataSetChanged() 
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)

Solution 5 - Android

Yes you can resolve this issue by making the adapter constructor only one time, I am explaining the coding part here :

if (appointmentListAdapter == null) {
        appointmentListAdapter = new AppointmentListAdapter(AppointmentsActivity.this);
        appointmentListAdapter.addAppointmentListData(appointmentList);
        appointmentListAdapter.setOnStatusChangeListener(onStatusChangeListener);
        appointmentRecyclerView.setAdapter(appointmentListAdapter);

    } else {
        appointmentListAdapter.addAppointmentListData(appointmentList);
        appointmentListAdapter.notifyDataSetChanged();
    }

Now you can see I have checked the adapter is null or not and only initialize when it is null.

If adapter is not null then I am assured that I have initialized my adapter at least one time.

So I will just add list to adapter and call notifydatasetchanged.

RecyclerView always holds the last position scrolled, therefore you don't have to store last position, just call notifydatasetchanged, recycler view always refresh data without going to top.

Thanks Happy Coding

Solution 6 - Android

The top answer by @DawnYu works, but the recyclerview will first scroll to the top, then go back to the intended scroll position causing a "flicker like" reaction which isn't pleasant.

To refresh the recyclerView, especially after coming from another activity, without flickering, and maintaining the scroll position, you need to do the following.

  1. Ensure you are updating you recycler view using DiffUtil. Read more about that here: https://www.journaldev.com/20873/android-recyclerview-diffutil
  2. Onresume of your activity, or at the point you want to update your activity, load data to your recyclerview. Using the diffUtil, only the updates will be made on the recyclerview while maintaining it position.

Hope this helps.

Solution 7 - Android

Here is an option for people who use DataBinding for RecyclerView. I have var recyclerViewState: Parcelable? in my adapter. And I use a BindingAdapter with a variation of @DawnYu's answer to set and update data in the RecyclerView:

@BindingAdapter("items")
fun setRecyclerViewItems(
    recyclerView: RecyclerView,
    items: List<RecyclerViewItem>?
) {
    var adapter = (recyclerView.adapter as? RecyclerViewAdapter)
    if (adapter == null) {
        adapter = RecyclerViewAdapter()
        recyclerView.adapter = adapter
    }

    adapter.recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState()
    // the main idea is in this call with a lambda. It allows to avoid blinking on data update
    adapter.submitList(items.orEmpty()) {
        adapter.recyclerViewState?.let {
            recyclerView.layoutManager?.onRestoreInstanceState(it)
        }
    }
}

Finally, the XML part looks like:

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/possible_trips_rv"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:items="@{viewState.yourItems}"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

Solution 8 - Android

I was making a mistake like this, maybe it will help someone :)

If you use recyclerView.setAdapter every time new data come, it calls the adapter clear() method every time you use it, which causes the recyclerview to refresh and start over. To get rid of this, you need to use adapter.notiftyDatasetChanced().

Solution 9 - Android

I have not used Recyclerview but I did it on ListView. Sample code in Recyclerview:

setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        rowPos = mLayoutManager.findFirstVisibleItemPosition();

It is the listener when user is scrolling. The performance overhead is not significant. And the first visible position is accurate this way.

Solution 10 - Android

1- You need to save scroll position like this

rvProduct.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                recyclerViewState = rvProduct.getLayoutManager().onSaveInstanceState(); // save recycleView state
            }
        });

2- And after you call notifyDataSetChanged then onRestoreInstanceState like this example

productsByBrandAdapter.addData(productCompareList);
productsByBrandAdapter.notifyDataSetChanged();
rvProduct.getLayoutManager().onRestoreInstanceState(recyclerViewState); // restore recycleView state

Solution 11 - Android

 mMessageAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
        @Override
        public void onChanged() {
            mLayoutManager.smoothScrollToPosition(mMessageRecycler, null, mMessageAdapter.getItemCount());
        }
    });

The solution here is to keep on scrolling recyclerview when new message comes.

The onChanged() method detects the action performed on recyclerview.

Solution 12 - Android

That's working for me in Kotlin.

  1. Create the Adapter and hand over your data in the constructor
class LEDRecyclerAdapter (var currentPole: Pole): RecyclerView.Adapter<RecyclerView.ViewHolder>()  { ... }
  1. change this property and call notifyDataSetChanged()
adapter.currentPole = pole
adapter.notifyDataSetChanged()

The scroll offset doesn't change.

Solution 13 - Android

Create Extention and use it entirely your App, if you are using DiffUtil you don't need to add adapter.notifyDataSetChanged()

fun RecyclerView.reStoreState(){
    val recyclerViewState = this.layoutManager?.onSaveInstanceState()
    this.layoutManager?.onRestoreInstanceState(recyclerViewState)
}

Then use it like this below

yourRecyclerView.reStoreState()
adapter.submitList(yourData)
yourRecyclerView.adapter = adapter

Solution 14 - Android

@BindingAdapter("items")
fun <T> RecyclerView.setItems(items: List<T>?) {
  (adapter as? ListAdapter<T, *>)?.submitList(items) {
    layoutManager?.onSaveInstanceState().let {
      layoutManager?.onRestoreInstanceState(it)
    }
  }
}

Solution 15 - Android

If you have one or more EditTexts inside of a recyclerview items, disable the autofocus of these, putting this configuration in the parent view of recyclerview:

android:focusable="true"
android:focusableInTouchMode="true"

I had this issue when I started another activity launched from a recyclerview item, when I came back and set an update of one field in one item with notifyItemChanged(position) the scroll of RV moves, and my conclusion was that, the autofocus of EditText Items, the code above solved my issue.

best.

Solution 16 - Android

Just return if the oldPosition and position is same;

private int oldPosition = -1;

public void notifyItemSetChanged(int position, boolean hasDownloaded) {
    if (oldPosition == position) {
        return;
    }
    oldPosition = position;
    RLog.d(TAG, " notifyItemSetChanged :: " + position);
    DBMessageModel m = mMessages.get(position);
    m.setVideoHasDownloaded(hasDownloaded);
    notifyItemChanged(position, m);
}

Solution 17 - Android

I had this problem with a list of items which each had a time in minutes until they were 'due' and needed updating. I'd update the data and then after, call

orderAdapter.notifyDataSetChanged();

and it'd scroll to the top every time. I replaced that with

 for(int i = 0; i < orderArrayList.size(); i++){
                orderAdapter.notifyItemChanged(i);
            }

and it was fine. None of the other methods in this thread worked for me. In using this method though, it made each individual item flash when it was updated so I also had to put this in the parent fragment's onCreateView

RecyclerView.ItemAnimator animator = orderRecycler.getItemAnimator();
    if (animator instanceof SimpleItemAnimator) {
        ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
    }

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKonrad MorawskiView Question on Stackoverflow
Solution 1 - AndroidDawnYuView Answer on Stackoverflow
Solution 2 - AndroidKaerdanView Answer on Stackoverflow
Solution 3 - AndroidA. SteenbergenView Answer on Stackoverflow
Solution 4 - AndroidMorten HolmgaardView Answer on Stackoverflow
Solution 5 - AndroidAmit Singh TomarView Answer on Stackoverflow
Solution 6 - AndroidMoses MwongelaView Answer on Stackoverflow
Solution 7 - Androidmacros013View Answer on Stackoverflow
Solution 8 - AndroidFatih GürsesView Answer on Stackoverflow
Solution 9 - AndroidThe Original AndroidView Answer on Stackoverflow
Solution 10 - AndroidHoussin BoullaView Answer on Stackoverflow
Solution 11 - AndroidAbhishek ChudekarView Answer on Stackoverflow
Solution 12 - AndroidChris8447View Answer on Stackoverflow
Solution 13 - AndroidJimale AbdiView Answer on Stackoverflow
Solution 14 - AndroidChanghoonView Answer on Stackoverflow
Solution 15 - AndroidAkhha8View Answer on Stackoverflow
Solution 16 - Androidraditya gumayView Answer on Stackoverflow
Solution 17 - AndroidMattView Answer on Stackoverflow