I designed an app using jetpack-navigation
There is a problem as illustrated in the following image when I move from one fragment to another one the status of the list disappears.
In fact, when returning from a layout, the article will be re-created in the stack and the list status will not be saved and the user will have to scroll again. Please help me?
jetpack-navigation
The fragment gets recreated on every navigation operation. You could store the scroll position in your activity and load it from there. But doing it like this, the scroll position will be lost on activity recreate (e.g. rotating device).
Better approch would be to store it in a ViewModel. (see https://developer.android.com/topic/libraries/architecture/viewmodel)
The view model survives activity recreation and you can store your scroll position.
Then you can load this position and tell the list to scroll to this position (e.g. for RecyclerView with LinearLayoutManager by calling scrollToPositionWithOffset(...))
I'm reloading recyclerView data every 15s. To keep scroll position when switching between apps, I use onSaveInstanceState() and onRestoreInstanceState(mRVState) methods in corresponding fragment overriden methods. But when I wanted to save position while switching between different fragments, I came up with this solution:
1.Set RecyclerView.OnScrollListener() in onResume() method of Fragment and get current first visible item position on each scroll. As you can see position variable is located in parent activity, so it's not lost on fragment replacement:
override fun onResume() {
super.onResume()
if (updateListRunnable != null) setAndRunUpdateListRunnable()
mRV?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
mainActivity.lastRVPosition =
(recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
}
})
}
2.Use scrollToPosition() method of recyclerView after data has been replaced inside adapter:
private fun setDataList(dataList: List<Data?>?) {
val mutableDataList = dataList?.toMutableList()
val currentItemCount = binding?.rvDataList?.adapter?.itemCount
if (currentItemCount == null || currentItemCount == 0) {
// create new adapter with initial data
val adapter = DataListAdapter(mutableDataList, baseVM, mainVM)
binding?.rvDataList?.adapter = adapter
binding?.rvDataList?.layoutManager = LinearLayoutManager(context)
mRV?.scrollToPosition(mainActivity.lastRVPosition);
} else {
// update existing adapter with updated data
mRVState = mRV?.layoutManager?.onSaveInstanceState()
val currentAdapter = binding?.rvDataList?.adapter as? DataListAdapter
currentAdapter?.updateDataList(dataList)
currentAdapter?.notifyDataSetChanged()
mRV?.layoutManager?.onRestoreInstanceState(mRVState)
mRV?.scrollToPosition(mainActivity.lastRVPosition);
}
}
As you can see I also use onSaveInstanceState()/onRestoreInstanceState() before/after replacing data, so that if there was no scroll before data replacement, position will still be saved. Scroll listener saved position is only useful when switching between fragments.
Related
I have custom ViewPager and PagerAdapter which is loading items inside instantiateItem. It is working fine if I initialize it for the first time with set list of items.
But as I call refresh on the list and I want to populate Adapter with new (totally different) list, after calling viewPager.adapter?.notifyDataSetChanged(), PagerAdapter stops working properly and those items are blank pages without content and I can swipe through them in UI.
PageScreen is not Fragment. It is just ViewGroup container which is inflating layout and setting values out of specific item. It is similar to ViewHolder + Binder in RecyclerView.
Also instatiateItem() is called only once as i add new list and call notifyDataSetChanged(). At start it is called 3 items, which is amount of PageScreen items in first list.
//init
val pages = mutableListOf<PageScreen>()
pages.add(PageScreen(activity, app, itemJs1, onClick = {onItemClicked(itemJs1.id)}))
pages.add(PageScreen(activity, app, itemJs2, onClick = {onItemClicked(itemJs2.id)}))
pages.add(PageScreen(activity, app, itemJs3, onClick = {onItemClicked(itemJs3.id)}))
swipePager.adapter = CustomPagerAdapter(pages).also { it.notifyDataSetChanged() }
...
//on refresh after API call
pages.clear()
contentList.forEach{item-> pages.add(PageScreen(activity, app, item, onClick = {onItemClicked(item.id)}))}
(swipePager.adapter as? CustomPagerAdapter)?.notifyDataSetChanged()
Also tried this (same result):
//on refresh after API call
val newPages = mutableListOf<PageScreen>()
contentList.forEach{item-> newPages.add(PageScreen(activity, app, item, onClick = {onItemClicked(item.id)}))}
swipePager.adapter = CustomPagerAdapter(newPages).also { it.notifyDataSetChanged() }
Adapter:
class CustomPagerAdapter(private var pageList: MutableList<PageScreen>) : PagerAdapter() {
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}
override fun getCount(): Int {
return pageList.size
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
override fun instantiateItem(container: ViewGroup, position: Int): View {
val layout = pageList[position].getScreen(container)
container.addView(layout)
return layout
}
}
Also I tried to properly refresh items (I expect that this is done internally by PagerAdapter and ViewPager when I call notifyDataSetChanged()) by removing them from ViewPager contentView and calling instantiateItem() for each item. But same result as above. Now every single page was blank. Function below is added to CustomPagerAdapter.
fun refreshItems(vp: ViewPager, data: MutableList<PageScreen>){
pageList.apply {
forEachIndexed{pos, item->
item.screenView?.let { sv->
destroyItem(vp, pos, sv)
}
}
clear()
addAll(data)
forEachIndexed { pos, _ -> instantiateItem(vp, pos) }
}
notifyDataSetChanged()
}
UPDATE:
I managed to "fix" this by setting ViewPager height to fixed value instead WRAP_CONTENT but its not a solution. I want ViewPager with dynamic height, because some of its children can have different height + setting something to static is not good approach in Android. Some phones with square displays could have cropped page then.
What happened is as I replaced all items, those "blank" pages were items with 0px height and 0px width for some unknown reason.
If I replaced ViewPager height to dp value, it "worked". But as I replaced those Views, first item was always blank. but as I scrolled to third one and back to first, item was there for some reason.
Also I don't get that height problem. I have function inside ViewPager which is setting its height based on tallest child in list. It works if list is static, but it is not working now as I refresh that.
Recreating whole ViewPagerAdapter is a solution for this problem.
I called API inside onPageSelected() and remembered position returned to listener function.
Then I recreated whole adapter like this:
swipePager.apply {
adapter = null
adapter = CustomPagerAdapter(newPages)
adapter?.notifyDataSetChanged()
invalidate()
}
and after refresh I scrolled to remembered position like this:
setCurrentItem(position, true)
This solution will not left blank pages, but I had to set static height for my ViewPager, which can be a problem on mdpi screens which sometimes cannot redraw particular dp value from XML layout correctly. Phone with this problem is for example Sony Xperia E5 which has xhdpi screen and part of my ViewPager is cropped from the bottom.
This have to be tuned manually with separate dimens.xml for both mdpi and xhdpi.
I have a RecyclerView inside of a Fragment. I use Data Binding and Navigation Component.
My problem is, when I go back to the previous fragment using the back arrow or just navigate back, and after that I open again the fragment, where I have the RecyclerView, the last item will be still there, but the list is totally empty.
I call this method when I close and open the fragment with the RecyclerView.
fun removeAllItems()
{
listOfItems.clear()
notifyItemRangeRemoved(0, listOfItems.lastIndex)
}
I have tried notifyDataSetChanged() also, but still I have the same issue.
Any idea why does it happen?
Thanks.
EDIT:
Finally I could solve the problem.
The onCreate() method:
private lateinit var listItemAdapter: ListItemAdapter
listItemAdapter = ListItemAdapter(context, mutableListOf<ListItem>())
Log.d(TAG, "Size_4: ${listItemAdapter.itemCount}")
listItemAdapter.removeAllItems()
binding.rvCreateNewListFourthItem.apply {
layoutManager = LinearLayoutManager(context)
adapter = listItemAdapter
}
Log.d(TAG, "Size_6: ${listItemAdapter.itemCount}")
Results: Size_4: 0
Results: Size_6: 0
After thse lines I have only 2 setOnClickListeners, which opens 2 other dialogs.
override fun onResume()
{
super.onResume()
Log.d(TAG, "Size_3: ${listItemAdapter.itemCount}")
listItemAdapter.removeAllItems()
Log.d(TAG, "Size_5: ${listItemAdapter.itemCount}")
}
Results: Size_3: 1
Results: Size_5: 0
The solution was to call the removeAllItems() method on the adapter.
When the fragment reached the onResume() method, the list contained the last item from the last session.
I am using a RecyclerView to show list of products in my app, I need to group the product based on aisle. while the data are fetched for the first time in the list, the products are grouped correctly with respect to aisle. When we scroll the view, the aisle group divider is shown for the wrong item and the divider gets restored to correct position once the onBindViewHolder gets refreshed automatically.
MyAdapter.class
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
itemsGrouping(pickItem, pickItemView, holder.adapterPosition)
}
private fun itemsGrouping(pickItem: PickItem, pickItemView: View, adapterPosition: Int) {
//Based on some condition
if(SomeCondition)
itemDivider(pickItemView,true)
else
itemDivider(pickItemView,false)
}
private fun itemDivider(v: View, boolean: Boolean) {
if(boolean) {
v.visibility = View.VISIBLE
} else {
v.visibility = View.GONE
}
}
Well, you should know that the view holders are reused in the RecyclerView, so it's probable not the right idea to try to determine the visibility of the divider in onBindViewHolder. I would recommend using item decorator for dividers. Here's the question and answer for that
How to add dividers and spaces between items in RecyclerView?
The problem is RecyclerView recycles previous views in order to be efficient.
I guess "SomeCondition" contains artifacts which are from previous holders.
So at
itemsGrouping(pickItem, pickItemView, holder.adapterPosition)
you should get pickItem and pickItemView from newly bound holder. You should use like
pickItemView = holder.findViewById(R.id.pickItemView);
Or consider using DataBinding Library
Here is a good example (it's in Kotlin) : DataBoundListAdapter
Once you extend your adapter to DataBoundListAdapter and override bind() method, everything inside bind is executed for every row, so you won't get repeated results.
Note : notice "executePendingBindings()"
Background
I work on an app that has a RecyclerView which you can scroll up and down however you wish.
The data items are loaded from the server, so if you are about to reach the bottom or the top, the app gets new data to show there.
To avoid weird scrolling behavior, and staying on the current item, I use 'DiffUtil.Callback' , overriding 'getOldListSize', 'getNewListSize', 'areItemsTheSame', 'areContentsTheSame'.
I've asked about this here, since all I get from the server is a whole new list of items, and not the difference with the previous list.
The problem
The RecyclerView doesn't have only data to show. There are some special items in it too:
Since Internet connection might be slow, there is a header item and a footer item in this RecyclerView, which just have a special Progress view, to show you've reached the edge and that it will get loaded soon.
The header and footer always exist in the list, and they are not received from the server. It's purely a part of the UI, just to show things are about to be loaded.
Thing is, just like the other items, it needs to be handled by DiffUtil.Callback, so for both areItemsTheSame and areContentsTheSame, I just return true if the old header is the new header, and the old footer is the new footer:
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
when {
oldItem.itemType != newItem.itemType -> return false
oldItem.itemType == ItemType.TYPE_FOOTER || oldItem.itemType == AgendaItem.TYPE_HEADER -> return true
...
}
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return when {
oldItem.itemType == ItemType.TYPE_FOOTER || oldItem.itemType == ItemType.TYPE_HEADER -> true
...
}
}
}
Seems right? Well it's wrong. If the user is at the top of the list, showing the header, and the list gets updated with new items, the header will stay at the top, meaning the previous items you've seen will get pushed away by the new ones.
Example:
Before: header, 0, 1, 2, 3, footer
After: header, -3, -2, -1, 0, 1, 2, 3, footer
So if you stayed on the header, and the server sent you the new list, you still see the header, and below the new items, without seeing the old ones. It scrolls for you instead of staying on the same position .
Here's a sketch showing the issue. The black rectangle shows the visible part of the list.
As you can see, before loading, the visible part has the header and some items, and after loading it still has the header and some items, but those are new items that pushed away the old ones.
I need the header to be gone on this case, because the real content is below it. Instead of the area of the header, it might show other items (or a part of them) above it, but the visible position of the current items should stay where they are.
This issue only occurs when the header is shown, at the top of the list. In all other cases it works fine, because only normal items are shown at the top of the visible area.
What I've tried
I tried to find how to set DiffUtil.Callback to ignore some items, but I don't think such a thing exists.
I was thinking of some workarounds, but each has its own disadvantages:
A NestedScrollView (or RecyclerView) which will hold the header&footer and the RecyclerView in the middle, but this might cause some scrolling issues, especially due to the fact I already have a complex layout that depends on the RecyclerView (collapsing of views etc...).
Maybe in the layout of the normal items, I could put the layout of the header and footer too (or just the header, because this one is the problematic one). But this is a bad thing for performance as it inflates extra views for nothing. Plus it requires me to toggle hiding and viewing of the new views within.
I could set a new ID for the header each time there is an update from the server, making it as if the previous header is gone, and there is a totally new header at the top of the new list. However, this might be risky in the case of no real updates of the list at the top, because the header will be shown as if it's removed and then re-added.
The questions
Is there a way to solve this without such workarounds?
Is there a way to tell DiffUtil.Callback : "these items (header&footer) are not real items to scroll to, and these items (the real data items) should be" ?
I will try to explain what I see as a solution to your problem:
Step 1: Remove all the code for FOOTER and HEADER views.
Step 2: Add these methods that add and remove dummy model items in adapter based on the user scroll direction:
/**
* Adds loader item in the adapter based on the given boolean.
*/
public void addLoader(boolean isHeader) {
if (!isLoading()) {
ArrayList<Model> dataList = new ArrayList<>(this.oldDataList);
if(isHeader) {
questions.add(0, getProgressModel());
else {
questions.add(getProgressModel());
setData(dataList);
}
}
/**
* Removes loader item from the UI.
*/
public void removeLoader() {
if (isLoading() && !dataList.isEmpty()) {
ArrayList<Model> dataList = new ArrayList<>(this.oldDataList);
dataList.remove(getDummyModel());
setData(questions);
}
}
public MessageDetail getChatItem() {
return new Model(0, 0, 0, "", "", "")); // Here the first value is id which is set as zero.
}
And here is your rest of the adapter logic that you need to decide if the item is a loader item or an actual data item:
#Override
public int getItemViewType(int position) {
return dataList.get(position).getId() == 0 ? StaticConstants.ItemViewTypes.PROGRESS : StaticConstants.ItemViewTypes.CONTENT;
}
According to the view type, you can add a progress bar view holder in your adapter.
Step 3: use these methods in data loading logic:
While making the API call in onScrolled() method of recyclerView, you need to add a loader item before the api call and then remove it after the api call. Use the given adapter methods above. The coded in onScrolled should look a little like this:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy < 0) { //This is top scroll, so add a loader as the header.
recyclerViewAdapter.addLoader(true);
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (!recyclerViewAdapter.isLoading(true)) {
if (linearLayoutManager.findFirstCompletelyVisibleItemPosition() <= 2) {
callFetchDataApi();
}
}
}
} else {
if (!recyclerViewAdapter.isLoading(false)) {
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() >= linearLayoutManager.getItemCount() - 2) {
callFetchDataApi();
}
}
});
Now after the api call gives you the data you need. Simply remove the added loader from the list like this:
private void onGeneralApiSuccess(ResponseModel responseModel) {
myStreamsDashboardAdapter.removeLoader();
if (responseModel.getStatus().equals(SUCCESS)) {
// Manage your pagination and other data loading logic here.
dataList.addAll(responseModel.getDataList());
recyclerViewAdapter.setData(dataList);
}
}
And lastly, you need to avoid any scroll during data loading operation is add a logic method for that is isLoading() method. which is used in the code of method onScrolled():
public boolean isLoading(boolean isFromHeader) {
if (isFromHeader) {
return dataList.isEmpty() || dataList.get(0).getId() == 0;
} else {
return dataList.isEmpty() || dataList.get(dataList.size() -1).getId() == 0;
}
}
Let me know if you don't understand any of this.
I think for now, the solution I took will suffice. It's a bit weird, but I think it should work:
The header item gets a new id each time the list is different in its first real item. The footer always have the same id, because it's ok for it to move in the current way it works. I don't even need to check that its id is the same. The check of areItemsTheSame is as such for them:
oldItem.agendaItemType == AgendaItem.TYPE_HEADER -> return oldItem.id == newItem.id
oldItem.agendaItemType == AgendaItem.TYPE_FOOTER -> return true
This way, if the header belongs to a new list data, old one will be removed, and new one will be at the top.
It's not the perfect solution, as it doesn't really push the original header to be at the top, and theoretically it makes us "kinda" have 2 headers at the same time (one being removed and one being added) but I think it's good enough.
Also, for some reason, I can't use notifyItemChanged on the header and footer in case only they get updated (internet connection changes its state, so need to change the header&footer alone). Only notifyDataSetChanged works for some reason.
Still, if there is a more official way, could be nice to know.
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);
}