Android Fragment with FirebaseRecyclerAdapter looses scroll position on rotation - android

I am using firebase database with firebaseui's FirebaseRecyclerAdapter. I have followed the example here:
https://github.com/firebase/FirebaseUI-Android/tree/master/database
-except I'm using a fragment.
The app works fine and I am having no issues except that when the device is rotated the recycler view does not restore its scroll position and instead resets to the top item.
- for example user scrolls 8 items down and then rotates device. user should be in the same scroll position, but instead position has returned to top.
I guess I could just put in additional code to handle this and scroll to the correct position, but I know from experience that the RecyclerView should handle this on its own. does the the FirebaseRecyclerAdapter require me to handle this?

Answering my own question-
The only way I've been able to get this to work is to save state like this:
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mRvPositionIndex = mLayoutManager.findFirstVisibleItemPosition();
View startView = mRecyclerView.getChildAt(0);
mRvTopView = (startView == null) ? 0 : (startView.getTop() - mRecyclerView.getPaddingTop());
outState.putInt(RV_POS_INDEX, mRvPositionIndex);
outState.putInt(RV_TOP_VIEW, mRvTopView);
}
and then in onCreateView(), right after I set the adapter I do this
if (savedInstanceState != null) {
mRvPositionIndex = savedInstanceState.getInt(RV_POS_INDEX);
mRvTopView = savedInstanceState.getInt(RV_TOP_VIEW);
mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
public void onItemRangeInserted(int positionStart, int itemCount) {
mLayoutManager.scrollToPositionWithOffset(mRvPositionIndex, mRvTopView);
}
});
}
I've done a little testing so far but seems to work fine. If someone has a better solution please let me know.

I found also that solution that works fine even in a normal activity
in onCreate:
mDataObserver = new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
mLayoutManager.scrollToPositionWithOffset(mRvPositionIndex, 0);
}
};
myFirebaseRecyclerAdapter.registerAdapterDataObserver(mDataObserver);
don't forget to unregister the AdapterDataObserver.

Related

RecyclerView scrolls to top on notifyDataSetChanged in chat screen

I am trying to create messaging kind of screen using recyclerView which will start from bottom and will loadMore data when user reached top end of chat. But I am facing this weird issue.
My recyclerView scrolls to top on calling notifyDataSetChanged. Due to this onLoadMore gets called multiple times.
Here is my code:
LinearLayoutManager llm = new LinearLayoutManager(context);
llm.setOrientation(LinearLayoutManager.VERTICAL);
llm.setStackFromEnd(true);
recyclerView.setLayoutManager(llm);
** In Adapter
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (messages.size() > 8 && position == 0 && null != mLoadMoreCallbacks) {
mLoadMoreCallbacks.onLoadMore();
}
** In Activity
#Override
public void onLoadMore() {
// Get data from database and add into arrayList
chatMessagesAdapter.notifyDataSetChanged();
}
It's just that recyclerView scrolls to top. If scrolling to top stops, this issue will be resolved. Please help me to figure out the cause of this issue. Thanks in advance.
I think you shouldn't use onBindViewHolder that way, remove that code, the adapter should only bind model data, not listen scrolling.
I usually do the "onLoadMore" this way:
In the Activity:
private boolean isLoading, totallyLoaded; //
RecyclerView mMessages;
LinearLayoutManager manager;
ArrayList<Message> messagesArray;
MessagesAdapter adapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
mMessages.setHasFixedSize(true);
manager = new LinearLayoutManager(this);
manager.setStackFromEnd(true);
mMessages.setLayoutManager(manager);
mMessages.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (manager.findFirstVisibleItemPosition() == 0 && !isLoading && !totallyLoaded) {
onLoadMore();
isLoading = true;
}
}
});
messagesArray = new ArrayList<>();
adapter = new MessagesAdapter(messagesArray, this);
mMessages.setAdapter(adapter);
}
#Override
public void onLoadMore() {
//get more messages...
messagesArray.addAll(0, moreMessagesArray);
adapter.notifyItemRangeInserted(0, (int) moreMessagesArray.size();
isLoading = false;
}
This works perfeclty for me, and the "totallyLoaded" is used if the server doesn't return more messages, to stop making server calls. Hope it helps you.
You see, it's natural for List to scroll to the most top item when you insert new Items. Well you going in the right direction but I think you forgot adding setReverseLayout(true).
Here the setStackFromEnd(true) just tells List to stack items starting from bottom of the view but when used in combination with the setReverseLayout(true) it will reverse order of items and views so the newest item is always shown at the bottom of the view.
Your final layoutManager would seems something like this:
mLayoutManager = new LinearLayoutManager(getActivity());
mLayoutManager.setReverseLayout(true);
mLayoutManager.setStackFromEnd(true);
mRecyclerView.setLayoutManager(mLayoutManager);
DON'T call notifyDataSetChanged() on the RecyclerView. Use the new methods like notifyItemChanged(), notifyItemRangeChanged(), notifyItemInserted(), etc...
And if u use notifyItemRangeInserted()--
don't call setAdapter() method after that..!
This is my way to avoid scrollview move to top
Instead of using notifyDataSetChanged(), I use notifyItemRangeChanged();
List<Object> tempList = new ArrayList<>();
tempList.addAll(mList);
mList.clear();
mList.addAll(tempList);
notifyItemRangeChanged(0, mList.size());
Update:
For another reason, Your another view in the top is focusing so it will jump to top when you call any notifies, so remove all focuses by adding android:focusableInTouchMode="true" in the GroupView.
I do not rely on onBindViewHolder for these kind of things. It can be called multiple times for a position. For the lists which has load more option maybe you should use something like this after your recyclerview inflated.
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if ((((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition() == 0)) {
if (args.listModel.hasMore && null != mLoadMoreCallback && !loadMoreStarted) {
mLoadMoreCallbacks.onLoadMore();
}
}
}
});
Hope it helps.
I suggest you to use notifyItemRangeInserted method of RecyclerView.Adapter for LoadMore operations. You add a set of new items to your list so you do not need to notify whole dataset.
notifyItemRangeInserted(int positionStart, int itemCount)
Notify any registered observers that the currently reflected itemCount
items starting at positionStart have been newly inserted.
For more information:
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html
You need to nofity the item in specific range like below:
#Override
public void onLoadMore() {
// Get data from database and add into arrayList
List<Messages> messegaes=getFromDB();
chatMessagesAdapter.setMessageItemList(messages);
// Notify adapter with appropriate notify methods
int curSize = chatMessagesAdapter.getItemCount();
chatMessagesAdapter.notifyItemRangeInserted(curSize,messages.size());
}
Checkout Firebase Friendlychat source-code on Github.
It behaves like you want, specially at:
mFirebaseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
int friendlyMessageCount = mFirebaseAdapter.getItemCount();
int lastVisiblePosition = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
// If the recycler view is initially being loaded or the user is at the bottom of the list, scroll
// to the bottom of the list to show the newly added message.
if (lastVisiblePosition == -1 ||
(positionStart >= (friendlyMessageCount - 1) && lastVisiblePosition == (positionStart - 1))) {
mMessageRecyclerView.scrollToPosition(positionStart);
}
}
});
You have this issue because every time your condition be true you call loadMore method even loadMore was in running state, for solving this issue you must put one boolean value in your code and check that too.
check my following code to get more clear.
1- declare one boolean value in your adapter class
2- set it to true in your condition
3- set it to false after you've got data from database and notified your adapter.
so your code must be like as following code:
public class YourAdapter extend RecylerView.Adapter<.....> {
private boolean loadingDataInProgress = false;
public void setLoadingDataInProgress(boolean loadingDataInProgress) {
this.loadingDataInProgress = loadingDataInProgress
}
....
// other code
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (messages.size() > 8 && position == 0 && null != mLoadMoreCallbacks && !loadingDataInProgress){
loadingDataInProgress = true;
mLoadMoreCallbacks.onLoadMore();
}
......
//// other adapter code
}
in Activity :
#Override
public void onLoadMore() {
// Get data from database and add into arrayList
chatMessagesAdapter.notifyDataSetChanged();
chatMessagesAdapter. setLoadingDataInProgress(false);
}
This must fix your problem but I prefer to handle loadMore inside Activity or Presenter class with set addOnScrollListener on RecyclerView and check if findFirstVisibleItemPosition in LayoutManager is 0 then load data.
I've wrote one library for pagination, feel free to use or custom it.
PS: As other user mentioned don't use notifyDataSetChanged because this will refresh all view include visible views that you don't want to refresh those, instead use notifyItemRangeInsert, in your case you must notify from 0 to size of loaded data from database.
In your case as you load from top, notifyDataSetChanged will change scroll position to top of new loaded data, so you MUST use notifyItemRangeInsert to get good feel in your app
You need to nofity the item in specific range
#Override
public void onLoadMore() {
// Get data from database and add into arrayList
List<Messages> messegaes=getFromDB();
chatMessagesAdapter.setMessageItemList(messages);
// Notify adapter with appropriate notify methods
int curSize = chatMessagesAdapter.getItemCount();
chatMessagesAdapter.notifyItemRangeInserted(curSize,messages.size());
}

RecyclerView scroll in incorrect direction when new element is inserted

I use smoothScrollToPosition to scroll RecyclerView. It does scroll every time a new entry is inserted; but to the top, not to the bottom, which is the direction i want.
list_chat = (RecyclerView) findViewById(R.id.list_chat);
//Set up Layout Manager
linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setStackFromEnd(true);
list_chat.setLayoutManager(linearLayoutManager);
//set adapter
list_chat.setAdapter(adapter);
//set scroll
list_chat.post(new Runnable() {
#Override
public void run() {
list_chat.smoothScrollToPosition(adapter.getItemCount());
}
});
The adapter is from Firebase
adapter = new FirebaseRecyclerAdapter<ChatItem, ChatRecylerViewHolder>(ChatItem.class,R.layout.chat_item
,ChatRecylerViewHolder.class,queryChat ) {
#Override
protected void populateViewHolder(ChatRecylerViewHolder viewHolder, ChatItem model, int position) {
viewHolder.tvAuthorChat.setText(model.chatAuthor);
viewHolder.tvContentChat.setText(model.chatContent);
}
};
You do notice you are using linearLayoutManager.setStackFromEnd(true); this mean you first position is at the bottom. I suggest you a better option.
RecycleView didn't work the way listView work, you can scroll it with your layout manager something like this
linearLayoutManager.scrollToPositionWithOffset(position,offset);
Which position is the position you want to scroll to, offset is the offset within the current position. You could just use with one parameter as well.
linearLayoutManager.scrollToPosition(position);
Ok. I found the answer.
First, the old problem with my question: i thought list_chat.post is called whenever an item is inserted (turn out that is wrong). The reason for it keeps scrolling top is linearLayoutManager.setStackFromEnd(true);
Thus, the question comes down to Where to call the scrolling ?
The answer is : Since adapter manages data, it makes sense to guess that adapter will notify the insertion.
Here is the code
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
list_chat.smoothScrollToPosition(adapter.getItemCount());
}
});

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.

how to get RecyclerView position for scrolling

I have a fragment that contains a RecyclerView. When I return to the fragment from somewhere else, I do
if(null!=savedInstanceState) {
mScrollPosition = savedInstanceState.getInt(ITEM_POSITION);
mRecyclerView.scrollToPosition(mScrollPosition);
}
but now I don't know what to put inside onSaveInstanceState:
#Override
public void onSaveInstanceState(Bundle bundle){
super.onSaveInstanceState(bundle);
Log.d(TAG, "onSaveInstanceState");
mScrollPosition=mRecyclerView.getScrollPosition();//???
bundle.putInt(NITEM_POSITION,mScrollPosition);
}
There is no mRecyclerView.getScrollPosition().
update
after testing it turns out that using mRecyclerView.scrollToPosition is not fine enough to ensure a smooth user experience. In cases where the item views are tall enough, scrollToPosition(ofFirstVisibleItem) can be way off. So is there a finer solution to this?
You can get the scroll from the LayoutManager. For example:
mScrollPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
also check this answer How to save RecyclerView's scroll position using RecyclerView.State?

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