How to save RecyclerView's scroll position using RecyclerView.State?
Asked Answered
M

20

137

I have a question about Android's RecyclerView.State.

I am using a RecyclerView, how could I use and bind it with RecyclerView.State?

My purpose is to save the RecyclerView's scroll position.

Muggy answered 7/1, 2015 at 9:30 Comment(1)
M
42

How do you plan to save last saved position with RecyclerView.State?

You can always rely on ol' good save state. Extend RecyclerView and override onSaveInstanceState() and onRestoreInstanceState():

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        LayoutManager layoutManager = getLayoutManager();
        if(layoutManager != null && layoutManager instanceof LinearLayoutManager){
            mScrollPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
        }
        SavedState newState = new SavedState(superState);
        newState.mScrollPosition = mScrollPosition;
        return newState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        if(state != null && state instanceof SavedState){
            mScrollPosition = ((SavedState) state).mScrollPosition;
            LayoutManager layoutManager = getLayoutManager();
            if(layoutManager != null){
              int count = layoutManager.getItemCount();
              if(mScrollPosition != RecyclerView.NO_POSITION && mScrollPosition < count){
                  layoutManager.scrollToPosition(mScrollPosition);
              }
            }
        }
    }

    static class SavedState extends android.view.View.BaseSavedState {
        public int mScrollPosition;
        SavedState(Parcel in) {
            super(in);
            mScrollPosition = in.readInt();
        }
        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(mScrollPosition);
        }
        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
Meenen answered 14/1, 2015 at 23:57 Comment(10)
now I have another question, how , no , what is RecyclerView.State can do?Guendolen
does this really work? I have tried this, but I keep getting a runtime exception like this: MyRecyclerView$SavedState cannot be cast to android.support.v7.widget.RecyclerView$SavedStateGallium
On the restore instance state we need to pass the superState to its super class (the recytcler view) like this: super.onRestoreInstanceState(((ScrollPositionSavingRecyclerView.SavedState) state).getSuperState());Macarthur
This doesn't work if you extend the RecyclerView, you'll get BadParcelException.Anemograph
Do you extend the activity or the adapter?Carmarthenshire
@Nikola Despotoski I have a similar issue here: #52177312. I would appreciate any insights you have on how to solve.Bags
Didn't work for me. When I restored my Fragment, my LinearLayoutManager.childCount was always 0.Kassab
@HeathBorders try it now. I currently edited this post. A problem was with .childCount. Use .itemcount instead of it.Cyclopentane
None of this is necessary : if you use something like LinearLayoutManager / GridLayoutManager the "scroll position" is already saved automatically for you. (It's the mAnchorPosition in LinearLayoutManager.SavedState). If you're not seeing that, then something is wrong with how you are reapplying data.Warmongering
findFirstVisibleItemPosition isn't compiling.Foliole
F
253

Update

Starting from recyclerview:1.2.0-alpha02 release StateRestorationPolicy has been introduced. It could be a better approach to the given problem.

This topic has been covered on android developers medium article.

Also, @rubén-viguera shared more details in the answer below. https://mcmap.net/q/88450/-how-to-save-recyclerview-39-s-scroll-position-using-recyclerview-state

Old answer

If you are using LinearLayoutManager, it comes with pre-built save api linearLayoutManagerInstance.onSaveInstanceState() and restore api linearLayoutManagerInstance.onRestoreInstanceState(...)

With that, you can save the returned parcelable to your outState. e.g.,

outState.putParcelable("KeyForLayoutManagerState", linearLayoutManagerInstance.onSaveInstanceState());

, and restore restore position with the state you saved. e.g,

Parcelable state = savedInstanceState.getParcelable("KeyForLayoutManagerState");
linearLayoutManagerInstance.onRestoreInstanceState(state);

To wrap all up, your final code will look something like

private static final String BUNDLE_RECYCLER_LAYOUT = "classname.recycler.layout";

/**
 * This is a method for Fragment. 
 * You can do the same in onCreate or onRestoreInstanceState
 */
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
    super.onViewStateRestored(savedInstanceState);

    if(savedInstanceState != null)
    {
        Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
        recyclerView.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
    }
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, recyclerView.getLayoutManager().onSaveInstanceState());
}

Edit: You can also use the same apis with the GridLayoutManager, as it is a subclass of LinearLayoutManager. Thanks @wegsehen for the suggestion.

Edit: Remember, if you are also loading data in a background thread, you will need to a call to onRestoreInstanceState within your onPostExecute/onLoadFinished method for the position to be restored upon orientation change, e.g.

@Override
protected void onPostExecute(ArrayList<Movie> movies) {
    mLoadingIndicator.setVisibility(View.INVISIBLE);
    if (movies != null) {
        showMoviePosterDataView();
        mDataAdapter.setMovies(movies);
      mRecyclerView.getLayoutManager().onRestoreInstanceState(mSavedRecyclerLayoutState);
        } else {
            showErrorMessage();
        }
    }
Fichte answered 20/3, 2015 at 12:18 Comment(16)
This is clearly the best answer, why is this not accepted? Note that any LayoutManager has that, not only LinearLayoutManager, also GridLayoutManager, and if not its a false implementation.Brandibrandice
linearLayoutManager.onInstanceState() always returns null --> check the source code. Unfortunately not working.Mohair
@Andev, that is true for abstract class LayoutManager. All implementing subclasses (Linear, Grid, Staggered) returns a Parcelable. Here is my reference for LinearLayoutManager, grepcode.com/file/repository.grepcode.com/java/ext/…Paulenepauletta
Just a warning - I got NPE in certain cases, so do some null checks when using it - otherwise great. Simple.Granular
Does not work with stackfFromEnd. Duh, can something work as intended on Android some day?Lashley
this should definitely be the accepted answer. Haven't had any problem with that and it saves the exact scroll position and state.Provenance
Doesn't RecyclerView do the state save of its LayoutManager in its own onSaveInstanceState? Looking at v24 of the support lib...Electrify
so anyone know how to implement this for listview?Sapota
This works on a single RecyclerView but not if you have a ViewPager with a RecycerView on each page.Cowardice
@Ivan, I think it works in a fragment (just checked), but not in onCreateView, but after data loading. See @Farmaker answer here.Hennessy
@Gökhan Barış Aker I have a similar issue here: #52177312. I would appreciate any insights you have on how to solve.Bags
@Ivan, in order for this to work in your Fragment ensure your saving the new layout state after the data is added to your adapter and not within onViewStateRestored. This worked for me. ie: ` adapter.submitList(homeContentList) if (!homeContentList.isEmpty()) { contentRecyclerView.scrollToPosition(0) emptyContent.visibility = GONE contentRecyclerView.layoutManager?.onRestoreInstanceState(this.savedRecyclerLayoutState) }`Appassionato
What if you're not using a Fragment?Familiar
@Ivan Currently, adding <activity [..] android:configChanges="orientation|screenSize"> to AndroidManifest.xml worksJibber
as recyclerview is still in alpha it is the best answer.Aldredge
doesnt not work with gridviewTrek
C
101

Store

lastFirstVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();

Restore

((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(lastFirstVisiblePosition);

and if that doesn't work, try

((LinearLayoutManager) rv.getLayoutManager()).scrollToPositionWithOffset(lastFirstVisiblePosition,0)

Put store in onPause() and restore in onResume()

Contumelious answered 14/1, 2015 at 22:59 Comment(9)
This works, but you may need to reset the lastFirstVisiblePosition to 0 after restoring it. This case is useful when you have a fragment/activity that loads different data in one RecyclerView, for example, a file explorer app.Bats
Excellent! It satisfies both coming back from a detail activity to the list, and saving state on screen rotationArtemisia
what to do if my recyclerView is in SwipeRefreshLayout?Swear
Why would the first restore option not work, but the second would?Adactylous
What is rv? I'm having problems.Humpy
@MehranJalili it seems to be a RecyclerViewChetchetah
scrollToPosition doesn't need cast to linearlayoutmanagerLeckie
This solution will not work if you have large enough list items so that it can be the case the that no list item is completely visible. The "findFirstCompletelyVisibleItemPosition()" method will return -1.Ordinance
that should be the accepted answerAntitrust
M
42

How do you plan to save last saved position with RecyclerView.State?

You can always rely on ol' good save state. Extend RecyclerView and override onSaveInstanceState() and onRestoreInstanceState():

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        LayoutManager layoutManager = getLayoutManager();
        if(layoutManager != null && layoutManager instanceof LinearLayoutManager){
            mScrollPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
        }
        SavedState newState = new SavedState(superState);
        newState.mScrollPosition = mScrollPosition;
        return newState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        if(state != null && state instanceof SavedState){
            mScrollPosition = ((SavedState) state).mScrollPosition;
            LayoutManager layoutManager = getLayoutManager();
            if(layoutManager != null){
              int count = layoutManager.getItemCount();
              if(mScrollPosition != RecyclerView.NO_POSITION && mScrollPosition < count){
                  layoutManager.scrollToPosition(mScrollPosition);
              }
            }
        }
    }

    static class SavedState extends android.view.View.BaseSavedState {
        public int mScrollPosition;
        SavedState(Parcel in) {
            super(in);
            mScrollPosition = in.readInt();
        }
        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(mScrollPosition);
        }
        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
Meenen answered 14/1, 2015 at 23:57 Comment(10)
now I have another question, how , no , what is RecyclerView.State can do?Guendolen
does this really work? I have tried this, but I keep getting a runtime exception like this: MyRecyclerView$SavedState cannot be cast to android.support.v7.widget.RecyclerView$SavedStateGallium
On the restore instance state we need to pass the superState to its super class (the recytcler view) like this: super.onRestoreInstanceState(((ScrollPositionSavingRecyclerView.SavedState) state).getSuperState());Macarthur
This doesn't work if you extend the RecyclerView, you'll get BadParcelException.Anemograph
Do you extend the activity or the adapter?Carmarthenshire
@Nikola Despotoski I have a similar issue here: #52177312. I would appreciate any insights you have on how to solve.Bags
Didn't work for me. When I restored my Fragment, my LinearLayoutManager.childCount was always 0.Kassab
@HeathBorders try it now. I currently edited this post. A problem was with .childCount. Use .itemcount instead of it.Cyclopentane
None of this is necessary : if you use something like LinearLayoutManager / GridLayoutManager the "scroll position" is already saved automatically for you. (It's the mAnchorPosition in LinearLayoutManager.SavedState). If you're not seeing that, then something is wrong with how you are reapplying data.Warmongering
findFirstVisibleItemPosition isn't compiling.Foliole
T
28

I wanted to save Recycler View's scroll position when navigating away from my list activity and then clicking the back button to navigate back. Many of the solutions provided for this problem were either much more complicated than needed or didn't work for my configuration, so I thought I'd share my solution.

First save your instance state in onPause as many have shown. I think it's worth emphasizing here that this version of onSaveInstanceState is a method from the RecyclerView.LayoutManager class.

private LinearLayoutManager mLayoutManager;
Parcelable state;

        @Override
        public void onPause() {
            super.onPause();
            state = mLayoutManager.onSaveInstanceState();
        }

The key to getting this to work properly is to make sure you call onRestoreInstanceState after you attach your adapter, as some have indicated in other threads. However the actual method call is much simpler than many have indicated.

private void someMethod() {
    mVenueRecyclerView.setAdapter(mVenueAdapter);
    mLayoutManager.onRestoreInstanceState(state);
}
Trowbridge answered 11/5, 2017 at 2:51 Comment(3)
That's the accurate solution. :DHerpetology
for me onSaveInstanceState isnt called so saving state in onPause seems like a better option. im also poping the backstack to return to the fragment.Manufacturer
for the load in of state. which i do in onViewCreated. it only seems to work when im in debug mode and use break points? or if i add a delay and put the execution in a coroutine.Manufacturer
D
24

Update: Since version 1.2.0-alpha02 there's a new API to control when state restoration (including scroll position) happens.

RecyclerView.Adapter lazy state restoration:

Added a new API to the RecyclerView.Adapter class which allows Adapter to control when the layout state should be restored.

For example, you can call:

myAdapter.setStateRestorationStrategy(StateRestorationStrategy.WHEN_NOT_EMPTY);

to make RecyclerView wait until Adapter is not empty before restoring the scroll position.

See also:


All layout managers bundled in the support library already know how to save and restore scroll position.

The RecyclerView/ScrollView/whatever needs to have an android:id for its state to be saved.

By default, scroll position is restored on the first layout pass which happens when the following conditions are met:

  • a layout manager is attached to the RecyclerView
  • an adapter is attached to the RecyclerView

Typically you set the layout manager in XML so all you have to do now is

  1. Load the adapter with data
  2. Then attach the adapter to the RecyclerView

You can do this at any time, it's not constrained to Fragment.onCreateView or Activity.onCreate.

Example: Ensure the adapter is attached every time the data is updated.

viewModel.liveData.observe(this) {
    // Load adapter.
    adapter.data = it

    if (list.adapter != adapter) {
        // Only set the adapter if we didn't do it already.
        list.adapter = adapter
    }
}

The saved scroll position is calculated from the following data:

  • Adapter position of the first item on the screen
  • Pixel offset of the top of the item from the top of the list (for vertical layout)

Therefore you can expect a slight mismatch e.g. if your items have different dimensions in portrait and landscape orientation.

Donal answered 31/1, 2019 at 13:18 Comment(7)
This was it for me. I was setting the adapter initially then updating its data when available. As explained above, leaving it until data available makes it automatically scroll back to where it was.Dinerman
Thanks @Eugen. Is there any documentation on LayoutManager's saving and restoring scroll position?Appassionato
@Appassionato What kind of documentation would you expect? What I described is an implementation detail of LinearLayoutManager, so read its source code.Donal
Here's a link to LinearLayoutManager.SavedState: cs.android.com/androidx/platform/frameworks/support/+/…Donal
Thanks, @EugenPechanec. This worked for me for device rotations. I'm also using a bottom navigation view and when I switch to another bottom tab and come back, the list starts from the beginning again. Any ideas on why this is?Exchangeable
@Exchangeable Last time I checked bottom nav docs it's supposed to start at top. You would have to save and restore view hierarchy state or fragment state manually.Donal
@Eugene Yes, that's still the case unfortunately. But it seems to be a bit of work, so I'll wait for the Navigation component to provide this functionality to implement it in my app. Maybe it'll be more streamlined then.Exchangeable
I
22

I Set variables in onCreate(), save scroll position in onPause() and set scroll position in onResume()

public static int index = -1;
public static int top = -1;
LinearLayoutManager mLayoutManager;

@Override
public void onCreate(Bundle savedInstanceState)
{
    //Set Variables
    super.onCreate(savedInstanceState);
    cRecyclerView = ( RecyclerView )findViewById(R.id.conv_recycler);
    mLayoutManager = new LinearLayoutManager(this);
    cRecyclerView.setHasFixedSize(true);
    cRecyclerView.setLayoutManager(mLayoutManager);

}

@Override
public void onPause()
{
    super.onPause();
    //read current recyclerview position
    index = mLayoutManager.findFirstVisibleItemPosition();
    View v = cRecyclerView.getChildAt(0);
    top = (v == null) ? 0 : (v.getTop() - cRecyclerView.getPaddingTop());
}

@Override
public void onResume()
{
    super.onResume();
    //set recyclerview position
    if(index != -1)
    {
        mLayoutManager.scrollToPositionWithOffset( index, top);
    }
}
Inly answered 9/2, 2016 at 9:6 Comment(2)
Keep offset of item too. Nice. Thanks!Firstclass
This is not correct. You have to add margin of a view (CardView) too. I have tested this.Footloose
T
15

You don't have to save and restore the state by yourself anymore. If you set unique ID in xml and recyclerView.setSaveEnabled(true) (true by default) system will automatically do it. Here is more about this: http://trickyandroid.com/saving-android-view-state-correctly/

Triadelphous answered 7/3, 2017 at 17:37 Comment(0)
T
15

Beginning from version 1.2.0-alpha02 of androidx recyclerView library, it is now automatically managed. Just add it with:

implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"

And use:

adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY

The StateRestorationPolicy enum has 3 options:

  • ALLOW — the default state, that restores the RecyclerView state immediately, in the next layout pass
  • PREVENT_WHEN_EMPTY — restores the RecyclerView state only when the adapter is not empty (adapter.getItemCount() > 0). If your data is loaded async, the RecyclerView waits until data is loaded and only then the state is restored. If you have default items, like headers or load progress indicators as part of your Adapter, then you should use the PREVENT option, unless the default items are added using MergeAdapter. MergeAdapter waits for all of its adapters to be ready and only then it restores the state.
  • PREVENT — all state restoration is deferred until you set ALLOW or PREVENT_WHEN_EMPTY.

Note that at the time of this answer, recyclerView library is still in alpha03, but alpha phase is not suitable for production purposes.

Titustityus answered 5/5, 2020 at 9:22 Comment(8)
So much overwork saved. Else we had to save list click index, restore elements, prevent redrawing in onCreateView, apply scrollToPosition (or do as other answers) and yet the user could notice the difference after coming backSixtieth
Also, If I want to save RecyclerView position after navigation through app ... is there some tool for it?Cooky
@RubenViguera Why alpha is not for production purposes? I mean is that really bad?Cooky
@Cooky "Also, If I want to save RecyclerView position after navigation through app ... is there some tool for it?" Then I guess your fragments aren't being restored properly by Navigation library. You should review your code because your fragments are likely to be recreated on every movement, and not restored.Backlog
@Cooky "Why alpha is not for production purposes? I mean is that really bad?" Alpha is a term used for software being in development, its API is suitable to changes, and highly probable of having bugs (unstable). Beta term is used for software being in testing state. I mean, is a software in a process of stabilization where no new features are developed and just bugs are fixed. Release Candidate term is for software in a final state of testing. If no more critial bugs appears is the candidate to be the next stable (production) version.Backlog
I don't do anything special with navigation controller so my fragments will be recreated every time... I usually use inclusive popUpTo to mainNav when I don't want backstack contain e.g. login screen after successfull login. Also, I use a lot inclusive popUpTo fragment_x when I don't want 2 fragments loop between each otherCooky
@RubénViguera thanks for the answers. About Alpha I knew that for sure, I just thought that recycler view has pretty solid reliability when I used it. Everything worked fineCooky
There is an issue with this API. In fact if you have registered listener, the onscroll callback will be invoked with 0 deltas when the adapter restores the state during the next frame.Malmsey
S
10

Since I find that most options are too long or complicated, here is my short Kotlin option with viewBinding and viewModel for achieving this:

If you want to have your RecyclerView state lifecycle aware, put this code in your ViewModel, otherwise you can use a basic Repository and work from there:

private lateinit var state: Parcelable
fun saveRecyclerViewState(parcelable: Parcelable) { state = parcelable }
fun restoreRecyclerViewState() : Parcelable = state
fun stateInitialized() : Boolean = ::state.isInitialized

and this inside your onPause()

binding.recyclerView.layoutManager?.onSaveInstanceState()?.let { viewModel.saveRecyclerViewState(it) }

and finally this in your onResume()

if (viewModel.stateInitialized()) {
        binding.recyclerView.layoutManager?.onRestoreInstanceState(
            viewModel.restoreRecyclerViewState()
        )
    }

Enjoy :)

Sempstress answered 26/8, 2021 at 12:15 Comment(0)
P
7

I've had the same requirement, but the solutions here didn't quite get me across the line, due to the source of data for the recyclerView.

I was extracting the RecyclerViews' LinearLayoutManager state in onPause()

private Parcelable state;

Public void onPause() {
    state = mLinearLayoutManager.onSaveInstanceState();
}

Parcelable state is saved in onSaveInstanceState(Bundle outState), and extracted again in onCreate(Bundle savedInstanceState), when savedInstanceState != null.

However, the recyclerView adapter is populated and updated by a ViewModel LiveData object returned by a DAO call to a Room database.

mViewModel.getAllDataToDisplay(mSelectedData).observe(this, new Observer<List<String>>() {
    @Override
    public void onChanged(@Nullable final List<String> displayStrings) {
        mAdapter.swapData(displayStrings);
        mLinearLayoutManager.onRestoreInstanceState(state);
    }
});

I eventually found that restoring the instance state directly after the data is set in the adapter would keep the scroll-state across rotations.

I suspect this either is because the LinearLayoutManager state that I'd restored was being overwritten when the data was returned by the database call, or the restored state was meaningless against an 'empty' LinearLayoutManager.

If the adapter data is available directly (ie not contigent on a database call), then restoring the instance state on the LinearLayoutManager can be done after the adapter is set on the recyclerView.

The distinction between the two scenarios held me up for ages.

Pear answered 30/11, 2018 at 12:58 Comment(2)
this is actually still usable even after the update which used recyclerview policy, in cases you need to manually save the state of the recyclerview for example if you have horizontal recyclerview inside vertical recyclerview, the inner recyclerview will never save its scroll position so you need to manually do like in the answer. and save the state inside onViewRecycled and set it again in onBindViewHolder.Sledgehammer
here is link to how to include recyclerview inside other with different layout-manger orientation medium.com/@gsaillen95/…Sledgehammer
W
5

Here is my Approach to save them and maintain the state of recyclerview in a fragment. First, add config changes in your parent activity as below code.

android:configChanges="orientation|screenSize"

Then in your Fragment declare these instance method's

private  Bundle mBundleRecyclerViewState;
private Parcelable mListState = null;
private LinearLayoutManager mLinearLayoutManager;

Add below code in your @onPause method

@Override
public void onPause() {
    super.onPause();
    //This used to store the state of recycler view
    mBundleRecyclerViewState = new Bundle();
    mListState =mTrailersRecyclerView.getLayoutManager().onSaveInstanceState();
    mBundleRecyclerViewState.putParcelable(getResources().getString(R.string.recycler_scroll_position_key), mListState);
}

Add onConfigartionChanged Method like below.

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    //When orientation is changed then grid column count is also changed so get every time
    Log.e(TAG, "onConfigurationChanged: " );
    if (mBundleRecyclerViewState != null) {
        new Handler().postDelayed(new Runnable() {

            @Override
            public void run() {
                mListState = mBundleRecyclerViewState.getParcelable(getResources().getString(R.string.recycler_scroll_position_key));
                mTrailersRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);

            }
        }, 50);
    }
    mTrailersRecyclerView.setLayoutManager(mLinearLayoutManager);
}

This approch will reslove your problem. if You have diffrent layout manager then just replace upper layout manager.

Warmhearted answered 7/8, 2018 at 7:44 Comment(2)
You may not need a Handler for persisting data. Lifecycle methods will be sufficient enough for saving/loading.Haman
@sayaMahi Can you describe in a proper way. I also have seen that onConfigurationChanged prevent to call on the destroy method. so it is not a proper solution.Warmhearted
K
4

On Android API Level 28, I simply ensure that I set up my LinearLayoutManager and RecyclerView.Adapter in my Fragment#onCreateView method, and everything Just Worked™️. I didn't need to do any onSaveInstanceState or onRestoreInstanceState work.

Eugen Pechanec's answer explains why this works.

Kassab answered 24/1, 2019 at 6:12 Comment(0)
T
3

This is how I restore RecyclerView position with GridLayoutManager after rotation when you need to reload data from internet with AsyncTaskLoader.

Make a global variable of Parcelable and GridLayoutManager and a static final string:

private Parcelable savedRecyclerLayoutState;
private GridLayoutManager mGridLayoutManager;
private static final String BUNDLE_RECYCLER_LAYOUT = "recycler_layout";

Save state of gridLayoutManager in onSaveInstance()

 @Override
        protected void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, 
            mGridLayoutManager.onSaveInstanceState());
        }

Restore in onRestoreInstanceState

@Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);

        //restore recycler view at same position
        if (savedInstanceState != null) {
            savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
        }
    }

Then when loader fetches data from internet you restore recyclerview position in onLoadFinished()

if(savedRecyclerLayoutState!=null){
                mGridLayoutManager.onRestoreInstanceState(savedRecyclerLayoutState);
            }

..of course you have to instantiate gridLayoutManager inside onCreate. Cheers

Toilsome answered 27/2, 2018 at 15:48 Comment(0)
W
2

For me, the problem was that I set up a new layoutmanager every time I changed my adapter, loosing que scroll position on recyclerView.

Watcher answered 19/2, 2018 at 19:35 Comment(0)
D
2

You can either use adapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY which is introduced in recyclerview:1.2.0-alpha02

https://medium.com/androiddevelopers/restore-recyclerview-scroll-position-a8fbdc9a9334

but it has some issues such as not working with inner RecyclerView, and some other issues you can check out in medium post's comment section.

Or you can use ViewModel with SavedStateHandle which works for inner RecyclerViews, screen rotation and process death.

Create a ViewModel with saveStateHandle

val scrollState=
    savedStateHandle.getLiveData<Parcelable?>(KEY_LAYOUT_MANAGER_STATE)

use Parcelable scrollState to save and restore state as answered in other posts or by adding a scroll listener to RecyclerView and

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {

        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
               scrollState.value = mLayoutManager.onSaveInstanceState()
            }
        }
Datcha answered 28/9, 2020 at 1:12 Comment(0)
T
1

I would just like to share the recent predicament I encounter with the RecyclerView. I hope that anyone experiencing the same problem will benefit.

My Project Requirement: So I have a RecyclerView that list some clickable items in my Main Activity (Activity-A). When the Item is clicked a new Activity is shown with the Item Details (Activity-B).

I implemented in the Manifest file the that the Activity-A is the parent of Activity-B, that way, I have a back or home button on the ActionBar of the Activity-B

Problem: Every time I pressed the Back or Home button in the Activity-B ActionBar, the Activity-A goes into the full Activity Life Cycle starting from onCreate()

Even though I implemented an onSaveInstanceState() saving the List of the RecyclerView's Adapter, when Activity-A starts it's lifecycle, the saveInstanceState is always null in the onCreate() method.

Further digging in the internet, I came across the same problem but the person noticed that the Back or Home button below the Anroid device (Default Back/Home button), the Activity-A does not goes into the Activity Life-Cycle.

Solution:

  1. I removed the Tag for Parent Activity in the manifest for Activity-B
  2. I enabled the home or back button on Activity-B

    Under onCreate() method add this line supportActionBar?.setDisplayHomeAsUpEnabled(true)

  3. In the overide fun onOptionsItemSelected() method, I checked for the item.ItemId on which item is clicked based on the id. The Id for the back button is

    android.R.id.home

    Then implement a finish() function call inside the android.R.id.home

    This will end the Activity-B and bring Acitivy-A without going through the entire life-cycle.

For my requirement this is the best solution so far.

     override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_show_project)

    supportActionBar?.title = projectName
    supportActionBar?.setDisplayHomeAsUpEnabled(true)

}


 override fun onOptionsItemSelected(item: MenuItem?): Boolean {

    when(item?.itemId){

        android.R.id.home -> {
            finish()
        }
    }

    return super.onOptionsItemSelected(item)
}
Tactile answered 23/11, 2018 at 0:30 Comment(0)
O
1

I had the same problem with a minor difference, I was using Databinding, and I was setting recyclerView adapter and the layoutManager in the binding methods.

The problem was that data-binding was happening after onResume so it was resetting my layoutManager after onResume and that means it was scrolling back to original place every time. So when I stopped setting layoutManager in Databinding adaptor methods, it works fine again.

Olivine answered 10/8, 2019 at 12:5 Comment(1)
how did you achieve it?Salmon
L
1

Alternative way watch this,

First of all, define variables,

private LinearLayoutManager mLayoutManager;
int lastPosition;

public static final String MyPREFERENCES__DISPLAY = "MyPrefs_display" ;
SharedPreferences sharedpreferences_display;
SharedPreferences.Editor editor_display;

then,

mLayoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(mLayoutManager);

recyclerView.scrollToPosition(sharedpreferences_display.getInt("last_saved_position", 0));

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        lastPosition = mLayoutManager.findFirstVisibleItemPosition();
    }
});

if you want to save position by button,

btn.setOnClickListener(v -> {
    editor_display.putInt("last_saved_position", lastPosition).apply();
    Toast.makeText(Arama_Fav_Activity.this, "Saved", Toast.LENGTH_SHORT).show();
});
Leucotomy answered 31/12, 2021 at 5:55 Comment(0)
P
0

In my case I was setting the RecyclerView's layoutManager both in XML and in onViewCreated. Removing the assignment in onViewCreated fixed it.

with(_binding.list) {
//  layoutManager =  LinearLayoutManager(context)
    adapter = MyAdapter().apply {
        listViewModel.data.observe(viewLifecycleOwner,
            Observer {
                it?.let { setItems(it) }
            })
    }
}
Parette answered 21/4, 2020 at 19:17 Comment(0)
R
-1

Activity.java:

public RecyclerView.LayoutManager mLayoutManager;

Parcelable state;

    mLayoutManager = new LinearLayoutManager(this);


// Inside `onCreate()` lifecycle method, put the below code :

    if(state != null) {
    mLayoutManager.onRestoreInstanceState(state);

    } 

    @Override
    protected void onResume() {
        super.onResume();

        if (state != null) {
            mLayoutManager.onRestoreInstanceState(state);
        }
    }   

   @Override
    protected void onPause() {
        super.onPause();

        state = mLayoutManager.onSaveInstanceState();

    }   

Why I'm using OnSaveInstanceState() in onPause() means, While switch to another activity onPause would be called.It will save that scroll position and restore the position when we coming back from another activity.

Rematch answered 3/9, 2016 at 7:24 Comment(2)
The Public static field is not good. Global data and singletons are not good.Floe
@Floe I understood now.I changed savingstate to onpause to remove global.Rematch

© 2022 - 2024 — McMap. All rights reserved.