How to save RecyclerView's scroll position using RecyclerView.State? - android

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.

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://stackoverflow.com/a/61609823/892500
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();
}
}

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

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];
}
};
}

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);
}

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:
RecyclerView.Adapter#setStateRestorationPolicy
RecyclerView.Adapter.StateRestorationPolicy
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
Load the adapter with data
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.

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);
}
}

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/

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.

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 :)

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.

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.

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.

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

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

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

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:
I removed the Tag for Parent Activity in the manifest for Activity-B
I enabled the home or back button on Activity-B
Under onCreate() method add this line supportActionBar?.setDisplayHomeAsUpEnabled(true)
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)
}

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.

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();
});

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) }
})
}
}

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.

Related

How to keep data in recycler view when screen orientation from portrait to landscape?

I have a custom object array to show in recycler view and I'm using GridLayout manager for my recylerview and showing (2x4),(3x3) etc. squares.
When user click these squares it's color changed.
But when screen orientate, view is refreshing.
My question is how to keep this data (selected square's color ) when screen orientation ?
Create an interface:
public interface ClickCallback {
void onItemClicked(String id);
}
In your activity create a callback and pass it to your adapter like this:
ClickCallback callback = new ClickCallback() {
#Override
public void onItemClicked(String id) {
//your list item object must have a method to get color
color = yourDataList.get(id).getColor();
}
};
protected void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
bundle.putInt("color", color);
}
YourAdapter adapter = new YourAdapter (yourDataList, callback)
In your adapter don't forget to set OnClickListener and call appropriate method, it could be like this:
public ListItemViewHolder(View itemView) {
super(itemView);
//your code
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
callback.onItemClicked(getAdapterPosition());
}
});
}
}
In OnCreate extract your color data:
public void onCreate(Bundle bundle) {
if (bundle != null) {
value = bundle.getInt("color");
}
}
You can store statuses of each square in a proper format and save them
on onSaveInstanceState and retrieve them on onCreate method and manipulate the adapter as for your requirement.
Read more...
Hope this helps!
"But when screen orientate, view is refreshing."
on screen rotation a new instance of the activity is created
You can have a ViewModel class which stores your arraylist of data including their current state. When activity is recreated via rotation this will be persisted and you will get the correct state.
Ref : https://medium.com/androiddevelopers/viewmodels-a-simple-example-ed5ac416317e

How to disable animation for ListAdapter

I am using new ListAdapter, which automatically animates changes. I would like to disable animations or enable/disable it programmatically.
class UserAdapter extends ListAdapter<User, UserViewHolder> {
public UserAdapter() {
super(User.DIFF_CALLBACK);
}
#Override
public void onBindViewHolder(UserViewHolder holder, int position) {
holder.bindTo(getItem(position));
}
public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
new DiffUtil.ItemCallback<User>() {
#Override
public boolean areItemsTheSame(
#NonNull User oldUser, #NonNull User newUser) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldUser.getId() == newUser.getId();
}
#Override
public boolean areContentsTheSame(
#NonNull User oldUser, #NonNull User newUser) {
// NOTE: if you use equals, your object must properly override Object#equals()
// Incorrectly returning false here will result in too many animations.
return oldUser.equals(newUser);
}
}
}
Another solution is to simply remove the item animator altogether.
recyclerView.itemAnimator = null
You could try to disable or enable animations with setSupportsChangeAnimations on RecyclerView item animator:
SimpleItemAnimator itemAnimator = (SimpleItemAnimator) recyclerView.getItemAnimator();
itemAnimator.setSupportsChangeAnimations(false);
Solution which bypasses DiffUtil
Setting itemAnimator to null and calling submitList() still runs DiffUtil.ItemCallback in a background thread and doesn't submit the list on the same frame! If you want the same behaviour as calling notifyDataSetChanged(), you can do this:
adapter.submitList(null)
adapter.submitList(newItems)
This is useful if you know the contents are indeed completely different and don't care if you loose the scroll position. Or if you have multiple RecyclerViews that all have to update on the same frame (to reduce screen flashing).
Why does this work?
Looking at the source code of AsyncListDiffer.submitList():
// fast simple remove all
if (newList == null) {
//noinspection ConstantConditions
int countRemoved = mList.size();
mList = null;
mReadOnlyList = Collections.emptyList();
// notify last, after list is updated
mUpdateCallback.onRemoved(0, countRemoved);
onCurrentListChanged(previousList, commitCallback);
return;
}
// fast simple first insert
if (mList == null) {
mList = newList;
mReadOnlyList = Collections.unmodifiableList(newList);
// notify last, after list is updated
mUpdateCallback.onInserted(0, newList.size());
onCurrentListChanged(previousList, commitCallback);
return;
}
When you call submitList() the first time, all items are immediately removed. The second time they are immediately inserted, never calling DiffUtil or starting a background thread computation.
Putting it all together
if(animations) {
adapter.submitList(newItems)
} else {
recyclerView.itemAnimator = null
adapter.submitList(null)
adapter.submitList(newItems) {
recyclerView.post {
//Restore the default item animator
recyclerView.itemAnimator = DefaultItemAnimator()
}
}
}

How to reset recyclerView position item views to original state after refreshing adapter

I have a RecyclerView with rows that have views that when clicked will be disabled for that row position.
The problem is after I update the adapter like this:
adapterData.clear();
adapterData.addAll(refreshedAdapterData);
notifyDataSetChanged();
After refreshing the data, the disabled views at the previous recycler position still remain disabled even though the data is refreshed.
How can I reset the views to the original state after refreshing adapter data.
Use below code.
adapterData.clear();
adapterData.addAll(refreshedAdapterData);
adapter.notifyDataSetChanged();
OR
recyclerView.invalidate();
When you call notifyDataSetChanged(), the onBindViewHolder() method of every view is called. So you could add something like this in the onBindViewHolder() of your Adapter method:
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, final int position) {
if (refreshedAdapterData.get(position).isInDefaultState()) {
//set Default View Values
} else {
//the code you already have
}
}
I have resolved this by putting a conditional statement inside onBindViewHolder method instructing all positions to reset the disabled views if data meets the required conditions for a refreshed data.
#Christoph Mayr, thanks for your comments. It helped point me in the right direction.
I cleared the data then notify change but the selected checkbox doesn't reset but just moves up one position. Let say I selected item #1 , move out of the RecyclerView, came back and it will auto select item #0.
So, I created new adapter again at onResume(), i worked for me but i don't know if it's the right way to handle this situation.
#Override
public void onResume() {
super.onResume();
if(selectedItems != null && selectedItems.size() > 0){
selectedItems.clear(); // if no selected items before then no need to reset anything
if(adapter != null && recyclerView != null){
// to remove the checked box
adapter = null;
adapter = new MyAdapter(items, new MyAdapter.MyAdapterListener() {
#Override
public void onSelected(int pos) {
selectedItems.add(items.get(pos));
}
#Override
public void onUnSelected(int pos) {
selectedItems.remove(items.get(pos));
}
});
recyclerView.setAdapter(adapter);
}
}
}

How to save scroll position of RecyclerView in Android?

I have Recycler view which lays inside of SwipeRefreshLayout. Also, have ability to open each item in another activity.
After returning back to Recycler I need scroll to chosen item, or to previous Y.
How to do that?
Yes, I googled, found articles in StackOverFlow about saving instance of layout manager, like this one: RecyclerView store / restore state between activities.
But, it doesn't help me.
UPDATE
Right now I have this kind of resolving problem, but, of course, it also doesn't work.
private int scrollPosition;
...//onViewCreated - it is fragment
recyclerView.setHasFixedSize(true);
LinearLayoutManager llm = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(llm);
data = new ArrayList<>();
adapter.setData(getActivity(), data);
recyclerView.setAdapter(adapter);
...
#Override
public void onResume() {
super.onResume();
recyclerView.setScrollY(scrollPosition);
}
#Override
public void onPause() {
super.onPause();
scrollPosition = recyclerView.getScrollY();
}
Yes, I have tried scrollTo(int, int) - doen't work.
Now I tried just scroll, for example, to Y = 100, but it doesn't scrolling at all.
Save the current state of recycle view position #onPause:
positionIndex= llManager.findFirstVisibleItemPosition();
View startView = rv.getChildAt(0);
topView = (startView == null) ? 0 : (startView.getTop() - rv.getPaddingTop());
Restore the scroll position #onResume:
if (positionIndex!= -1) {
llManager.scrollToPositionWithOffset(positionIndex, topView);
}
or another way can be #onPause:
long currentVisiblePosition = 0;
currentVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
restore #onResume:
((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(currentVisiblePosition);
currentVisiblePosition = 0;
A lot of these answers seem to be over complicating it.
The LayoutManager supports onRestoreInstanceState out of the box so there is no need to save scroll positions etc. The built in method already saves pixel perfect positions.
example fragment code (null checking etc removed for clarity):
private Parcelable listState;
private RecyclerView list;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
listState=savedInstanceState.getParcelable("ListState");
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable("ListState", list.getLayoutManager().onSaveInstanceState());
}
then just call
list.getLayoutManager().onRestoreInstanceState(listState);
once your data has been reattached to your RecyclerView
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.
User your recycler view linearlayoutmanager for getting scroll position
int position = 0;
if (linearLayoutManager != null) {
scrollPosition = inearLayoutManager.findFirstVisibleItemPosition();
}
and when restoring use following code
if (linearLayoutManager != null) {
cardRecyclerView.scrollToPosition(mScrollPosition);
}
Hope this helps you
to save position to Preferences, add this to your onStop()
int currentVisiblePosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
getPreferences(MODE_PRIVATE).edit().putInt("listPosition", currentVisiblePosition).apply();
then restore position like this
if (getItemCount() == 0) {
int savedListPosition = getPreferences(MODE_PRIVATE).getInt("listPosition", 0);
recyclerView.getLayoutManager().scrollToPosition(savedListPosition); }
this last code should be added inside an event of the Adapter (not sure witch event but in my case was onEvent() - com.google.firebase.firestore.EventListener)
For some reason there are a lot of quite misleading tips/suggestions on how to save and restore scroll position in your_scrolling_container upon orientation changes.
Taking current scroll position and saving it in Activity’s onSaveInstanceState
Extending a certain scrollable View to do same there
Preventing Activity from being destroyed on rotation
And yeah, they are working fine, but…
But in fact, everything is much simpler, because Android is already doing it for you!
If you take a closer look at
RecyclerView/ListView/ScrollView/NestedScrollView sources, you’ll see that each of them is saving its scroll position in onSaveInstanceState. And during the first layout pass they are trying to scroll to this position in onLayout method.
There are only 2 things you need to do, to make sure it’s gonna work fine:
Set an id for your scrollable view, which is probably already done. Otherwise Android won’t be able to save View state automatically.
Provide a data before the first layout pass, to have the same scroll boundaries you had before rotation. That’s the step where developers usually have some issues.
The easiest and transition compatible way I found is:
#Override
public void onPause() {
super.onPause();
recyclerView.setLayoutFrozen(true);
}
#Override
public void onResume() {
super.onResume();
recyclerView.setLayoutFrozen(false);
}
in onSaveInstanceState() method of fragment you can save the scroll position of RecycleView
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
LinearLayoutManager layoutManager = (LinearLayoutManager)
recyclerView.getLayoutManager();
outState.putInt("scrolled_position",
layoutManager.findFirstCompletelyVisibleItemPosition());
}
then you can retrieve saved scroll position in onViewStateRestored() method
#Override
public void onViewStateRestored(#Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
int scrollPosition = savedInstanceState.getInt("scrolled_position");
recyclerView.scrollToPosition(scrollPosition);
}
}
You can use scrollToPosition or smoothScrollToPosition to scroll to any item position in RecyclerView.
If you want to scroll to item position in adapter, then you would have to use adapter's scrollToPosition or smoothScrollToPosition.

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?
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!
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());
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 ---
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.
Store this variable in your Bundle, and restore it in state restoration to a different variable, we'll call it mStateScrollY.
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.
Keep scroll position by using #DawnYu answer to wrap notifyDataSetChanged() like this:
val recyclerViewState = recyclerView.layoutManager?.onSaveInstanceState()
adapter.notifyDataSetChanged()
recyclerView.layoutManager?.onRestoreInstanceState(recyclerViewState)
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
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.
Ensure you are updating you recycler view using DiffUtil. Read more about that here: https://www.journaldev.com/20873/android-recyclerview-diffutil
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.
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"/>
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().
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
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.
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
#BindingAdapter("items")
fun <T> RecyclerView.setItems(items: List<T>?) {
(adapter as? ListAdapter<T, *>)?.submitList(items) {
layoutManager?.onSaveInstanceState().let {
layoutManager?.onRestoreInstanceState(it)
}
}
}
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.
That's working for me in Kotlin.
Create the Adapter and hand over your data in the constructor
class LEDRecyclerAdapter (var currentPole: Pole): RecyclerView.Adapter<RecyclerView.ViewHolder>() { ... }
change this property and call notifyDataSetChanged()
adapter.currentPole = pole
adapter.notifyDataSetChanged()
The scroll offset doesn't change.
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.
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);
}
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);
}

Categories

Resources