129

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?

Konrad Morawski
  • 8,307
  • 7
  • 53
  • 91
  • It is about width and height values your RecyclerView or the LayoutManager. Check this solution: http://stackoverflow.com/questions/28993640/recyclerview-notifydatasetchanged-scrolls-to-top-position/ – serefakyuz Sep 06 '16 at 07:29
  • @Konard, In my case I have list of audio file with Seekbar implementation and running with following issue: 1) For few last indexed item as soon it triggered notifyItemChanged(pos) it push view at bottom automatic. 2) While keep on notifyItemChanged(pos) it stuck the list and do not allow to scroll easily. Though I'll ask as a question but let me know please if you got any better approach to fix such issue. – CoDe Oct 31 '17 at 13:14

17 Answers17

186

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!

Cory Petosky
  • 12,458
  • 3
  • 39
  • 44
DawnYu
  • 2,278
  • 1
  • 12
  • 14
55

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());
Kaerdan
  • 1,180
  • 10
  • 9
  • 4
    I tried adapter.notifyItemRangeChanged(0, adapter.getItemCount());, that too works as .notifyDataSetChanged() – Thamilan S Apr 09 '15 at 17:34
  • 4
    This helped in my case. I was inserting several items at position 0 and with `notifyDataSetChanged` the scroll position would change showing the new items. However, if use `notifyItemRangeInserted(0, newItems.size() - 1)` the `RecyclerView` keeps the scroll state. – Carlosph Jun 12 '15 at 07:21
  • very very good answer, i searching solution of that answer whole one day. thank you, so much.. – Gundu Bandgar Apr 22 '16 at 18:35
  • 1
    adapter.notifyItemRangeChanged(0, adapter.getItemCount()); should be avoided, as indicated in the latest Google I/O (watch the presentation on recyclerview in and outs), it would better (if you have the knowledge) to tell the recyclerview exactly which items changed instead of a flat catch-all refresh of all views. – A. Steenbergen May 25 '16 at 12:08
  • That is true, but in the question were very little information about what exactly changed. If i can rephrase, so it would be more clear, instead of "You should be more specific" -> "You should be as specific as possible". – Kaerdan Jun 03 '16 at 18:57
  • @Kaerdan how can i refresh recyclerview content without calling notifyDataSetChanged(). – Android Killer Jun 25 '16 at 17:32
  • perfect answer , don't know why not marked as answer :| – Dart Apr 23 '18 at 11:31
  • 6
    its not working, recyclerview refreshing to top even though i added – Shaahul Dec 10 '18 at 06:18
  • Awesome answer, this can also survive Leanback's focus when items are inserted/removed. Exactly what I was looking for. – Marek Teuchner Oct 03 '19 at 13:25
22

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.

A. Steenbergen
  • 3,360
  • 4
  • 34
  • 51
  • i don't understand you can you post full example ? –  Oct 20 '15 at 13:42
  • 1
    This is actually as close to a full example as you can get if you don't want me to post my whole activity – A. Steenbergen Feb 22 '16 at 17:12
  • I don't get something. If there were added items in the beginning of the list, won't staying on the same scrolling position be wrong, as you will actually now be looking on different items that took over this area ? – android developer Nov 23 '17 at 12:06
  • Yes, in that case you have to adjust the position when restoring, however, this is not part of the question, as usually while rotating you do not add or remove items. That would be strange and behaviour and probably not in the user's interest except for some special cases that might exist, which I cannot picture, honestly. – A. Steenbergen Nov 23 '17 at 16:09
20

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

val recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState() 
adapter.notifyDataSetChanged() 
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)
Morten Holmgaard
  • 7,484
  • 8
  • 63
  • 85
10

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

Masoud Mokhtari
  • 2,390
  • 1
  • 17
  • 45
6

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.

2

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"/>
macros013
  • 739
  • 9
  • 10
2

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().

2

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
Houssin Boulla
  • 2,687
  • 1
  • 16
  • 22
1

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.

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
The Original Android
  • 6,147
  • 3
  • 26
  • 31
  • Okay, `findFirstVisibleItemPosition` returns "the adapter position of the first visible view", but what about the offset. – Konrad Morawski Feb 25 '15 at 08:33
  • 1
    Is there a similar method for Recyclerview to setSelection method of ListView for the visible position? This is what I did for ListView. – The Original Android Feb 25 '15 at 08:54
  • 1
    How about using setScrollY method with the dy parameter, a value which you will save? If it works, I'll update my answer. I have not used Recyclerview but I want to in my future app. – The Original Android Feb 25 '15 at 08:59
  • Please see my answer below on how to save the scroll position. – A. Steenbergen May 12 '15 at 15:36
  • @Doge, I see that you submitted a detailed long answer/explanation, possibly good for the questioner, Konrad. You may add a comment to the original post so that he gets notified. If he gets to read your answer, he may appreciate it and select as the Best answer. – The Original Android May 12 '15 at 17:57
  • @Doge, However I noticed also you downvote all the other posted answers. Why? Do you disagree with the other ones? Are they not related to the question/issue? Do you just like your answer more than the rest? I think these reasons are not appropriate for downvoting posts. Pls read a relative "hot" discussion on link http://meta.stackoverflow.com/questions/252297/how-should-i-handle-downvote-complaints. If you want, you may undo the downvotes. – The Original Android May 12 '15 at 17:59
  • I am sorry, I downvoted only your answer, because it was not correct in my opinion, the other one I didn't vote on. – A. Steenbergen May 12 '15 at 20:31
  • Actually I cannot undo it it says it is locked, sorry for that, but you have to admit it is only half the answer, if not the wrong answer. – A. Steenbergen May 12 '15 at 20:32
  • @Doge, An answer does not have to be complete, just helpful or useful. Sometimes the developer needs just a little information to solve his/her problem. Do you agree? Many posts in SO are about giving little info. And then it's up to the developer to use the info and come up with a solution. About the downvote, no big deal/issue. the main thing is the principle of downvote. – The Original Android May 12 '15 at 20:40
  • 1
    Alright, I will keep that in mind next time, I didn't downvote you with malintent. However I disagree with short and incomplete answers because when I started out the short answers where other developers assumed a lot of background info did not really help me, because they were often too incomplete and I had to ask a lot of clarifying questions, something you usually cannot do on old stackoverflow posts. Also notice how no answer was accepted by Konrad, which indicates that these answers did not solve his problem. Though I see your general point. – A. Steenbergen May 12 '15 at 20:45
1

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
Jimale Abdi
  • 2,574
  • 5
  • 26
  • 33
1
@BindingAdapter("items")
fun <T> RecyclerView.setItems(items: List<T>?) {
  (adapter as? ListAdapter<T, *>)?.submitList(items) {
    layoutManager?.onSaveInstanceState().let {
      layoutManager?.onRestoreInstanceState(it)
    }
  }
}
Hun
  • 3,652
  • 35
  • 72
0
 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.

aac
  • 574
  • 2
  • 6
  • 18
0

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.

Chris8447
  • 306
  • 4
  • 7
-2

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.

Akhha8
  • 418
  • 3
  • 10
-4

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);
}
raditya gumay
  • 2,951
  • 3
  • 17
  • 24
-5

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);
    }
Matt
  • 59
  • 5