I need to refresh / rebind ListView or RecyclerView contents without refreshing the header item itself.
Any tips on how to achieve this?
Thanks.
Yes, you can do this. Generally, your header has 0 position in the list, so the header places on the top of your list. So, for your lists, e.g. RecyclerView you must initialize the adapter (in case of RecyclerView you must create accessor of RecyclerView.Adapter class) and this adapter has a lot of methods for updating data in adapter (notifyDataSetChanged(), notifyItemInserted() etc.) and you can use one of this methods, depends on your purpose. So, in your case you can use notifyItemRangeChanged(int positionStart, int itemCount).
You can find more information about these methods in the official documentation
Assuming the header is at position 0:
Lets say you want to bind the header once and then stop binding it after refresh:
When you call notifyDataSetChanged() to reload, the onBindViewHolder() method in the adapter gets called again to refresh the data, keeping that in mind. You can set a boolean so that you bind your header once. So that eventhough the onBindViewHolder() is called multiple times the header would bind once.
class Adapter extends ...........{
//use a boolean as a flag
private boolean bindHeader = true;
........
.......
.......
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
..........
if(position == 0 && bindHeader){
//bind the header only for the first time
......
......
//then stop binding after refresh
bindHeader = false;
}else if(position > 0){
//bind the reset of the items here
}
}
..............
..............
}
Related
I have a simple recyclerview with items (tips) and a loading spinner at the bottom.
here's how the item count and item view type methods look:
#Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1) { // last position
return LOADING_FOOTER_VIEW_TYPE;
}
else {
return TIP_VIEW_TYPE;
}
}
#Override
public int getItemCount() {
return tips.size() + 1; // + 1 for the loading footer
}
basically, i just have a loading spinner under all my items.
I create the adapter once like so:
public TipsListAdapter(TipsActivity tipsActivity, ArrayList<Tip> tips) {
this.tipsActivity = tipsActivity;
this.tips = tips;
}
and then once i have fetched additional items, i call add like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeInserted(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
What's odd here is that when i do that, the scroll position goes to the very bottom. It almost seems like it followed the loading spinner. This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
This doesn't happen if i change notifyItemRangeInserted() to notifyItemRangeChanged() like so:
public void addTips(List<Tip> tips) {
// hide the loading footer temporarily
isAdding = true;
notifyItemChanged(getItemCount() - 1);
// insert the new items
int insertPos = this.tips.size(); // this will basically give us the position of the loading spinner
this.tips.addAll(tips);
notifyItemRangeChanged(insertPos, tips.size());
// allow the loading footer to be shown again
isAdding = false;
notifyItemChanged(getItemCount() - 1);
}
Nor does it happen if i simply call notifyDataSetChanged() like so:
public void addTips(List<Tip> tips) {
this.tips.addAll(tips);
notifyDataSetChanged();
}
Here's the code for setting the adapter in my Activity:
public void setAdapter(#NonNull ArrayList<Tip> tips) {
if (!tips.isEmpty()) { // won't be empty if restoring state
hideProgressBar();
}
tipsList.setAdapter(new TipsListAdapter(this, tips));
}
public void addTips(List<Tip> tips) {
hideProgressBar();
getAdapter().addTips(tips);
restorePageIfNecessary();
}
private TipsListAdapter getAdapter() {
return (TipsListAdapter) tipsList.getAdapter();
}
Note:
I don't manually set scroll position anywhere.
I call setAdapter() in onResume()
addTips() is called after I fetch items from the server
Let me know if you need any additional parts of my code.
This only happens on the first add (i.e. when there is only the loading spinner showing initally). subsequent adds maintains the proper scroll position (the position where the items were inserted).
RecyclerView has built-in behavior when calling the more-specific dataset change methods (like notifyItemRangeInserted() as opposed to notifyDataSetChanged()) that tries to keep the user looking at "the same thing" as before the operation.
When the data set changes, the first item the user can see is prioritized as the "anchor" to keep the user looking at approximately the same thing. If possible, the RecyclerView will try to keep this "anchor" view visible after the adapter update.
On the very first load, the first item (the only item) is the loading indicator. Therefore, when you load the new tips and update the adapter, this behavior will prioritize keeping the loading indicator on-screen. Since the loading indicator is kept at the end of the list, this will scroll the list to the bottom.
On subsequent loads, the first item is not the loading indicator, and it doesn't move. So the RecyclerView will not appear to scroll, since it doesn't have to do so to keep the "anchor" on-screen.
My recommendation is to check insertPos and see if it is zero. If it is, that means this is the first load, so you should update the adapter by calling notifyDataSetChanged() in order to avoid this anchoring behavior. Otherwise, call notifyItemRangeInserted() as you're currently doing.
Remove the setAdapter code from onResume ASAP as you are setting new TipsListAdapter(this, tips);
Every time a new reference of the adapter is created...make field mAdapter and then set it in onCreate . RecyclerView doesnt remember the scrolled position because everytime a new reference of adapter is being created.. onResume gets called infinitely when activity is in running state..
So either you setAdapter in onCreate using new operator to create reference for adapter or,
in onResume use mAdapter field variable reference..
I have been working with RecyclerView for a while. I am following lazy loading, so I am showing 10 data on the view each time. If user scroll to the bottom, the page re-load from the TOP! however, I want to stay where it was previously! So, I have tried to use
recyclerView.scrollToPosition(position);
However, this breaks the UI flow!
My second try is using onSaveInstanceState() and onRestoreInstanceState(Bundle state); However, that does not work! My page is re-loads to the top!
Parcelable state = layoutManager.onSaveInstanceState();
layoutManager.onRestoreInstanceState(state);
I have tried every other methods, apparently none is working for me!
Any help would be highly appreciated! Thanks in advance!
Note : Make sure that you are not initialising or calling setAdapter() method each time after updating your dataset. If not, then
You have to update your data list and call notifyDataSetChanged() which will update your adapter from existing position.
Let's say you have stored your data into ArrayList mData;
Your getItemCount() would be
#Override
public int getItemCount() {
if (mData != null && mData.length() > 0)
return mData.size();
return 0;
}
Now create one more method in your adapter which you will called each time whenever you will get new data from server. This method will simply override your dataset and will update your adapter
private void updateDataSet(ArrayList<String> mData){
this.mData = mData;
notifyDataSetChanged();
}
I have done this functionality before 2 days ago
I share my idea with you
Make
Listview lv; //Assume Find view by Id
List<Model> models = new ArrayList();
Now Make an Adapter and assign blank models
CustomAdapter adapter = new CustomeAdapter(context,models);
lv.setAdapter(adapter);
Now when you have new data to load with lazylodaing
do this
models.addAll(newModels); //new ModelList
adapter.notifyDataSetChanged();
Thats it.
This is the default behaviour Recycler View to recycle/resuse views. As in official docs:
https://developer.android.com/reference/android/support/v7/widget/RecyclerView.html
If you are having any view related issues then save your view state in your List<Object> like setting visible item or not per position. And in your onBindViewHolder method as per position show/hide your view.
I want to update a view of the recyclerview when a notifyItemChanged has been called. The thing is that I don't want to refresh the entire row but only the view of the row. (to avoid the blinking effect of the row)
There is a method called notifyItemChanged(int, payload obj).
Can I use that to achieve that? If so how to do it?
Finally I found how to update only the specific view of a row in RecyclerView.
(1) Override onBindViewHolder(Recycler.ViewHolder VH, int position, List payloads) in the adapter
(2) Inside that onBindViewHolder method,
if(payloads != null && !payloads.isEmpty() && (payloads.get(0) instanceof customObject)){
// update the specific view
}else{
// I have already overridden the other onBindViewHolder(ViewHolder, int)
// The method with 3 arguments is being called before the method with 2 args.
// so calling super will call that method with 2 arguments.
super.onBindViewHolder(holder,position,payloads);
}
(3) So to notify data change and update the view, need to call the notifyItemChanged(int position, Object payload). Need to pass the customObject(model which holds the data) as the payload object.
adapter.notifyItemChanged(i, obj);
Note : Need to disable the RecyclerView's ItemAnimator(By default it is enabled). Otherwise payload object will be empty.
recyclerView.setItemAnimator(null);
or
((SimpleItemAnimator)recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
UPDATE: I used this method recently without disabling the ItemAnimator. So no need to disable ItemAnimator. - (recyclerview-v7:25.4.0)
for further reference
You can use this to get the view of updated item:
View v = recyclerView.getLayoutManager().findViewByPosition(position);
if (v != null){
//update your view
}
Let's say you have an ArrayList of items, and it is fed to the RecyclerView adapter. Let it be something like this.
List<String> items = new ArrayList<>();
RecyclerAdapter adapter = new RecyclerAdapter(items);
recyclerView.setAdapter(adapter);
Now as I can see you want to insert items into the RecyclerView and call notifyItemChanged(int position) method to add only the single item.
For this let's say you have changed the value of item in position 0. That's the first item in the list.
Now the proper usage of the method notifyItemChanged(int position) will be
adapter.notifyItemChanged(0);
This means, the adapter will update the recyclerview with the new value in 0 position.
I am getting data from Database and displaying using a custom list adapter in a ListView. I need to display only even position items in ListView.
i am able to solve this in two ways.
1. sorting data before attaching to adapter, but i want to do those task in getView() method of adapter/by using other available methods in adapter.
2.
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
if( position%2 == 0 ) {
// display
} else {
// not display
}
return view;
in this i am getting alternate view's are empty view's.. i want to avoid these empty view.
i ant to do those all calculations in getView() method, without empty view in the ListView. How i can do this?
I will suggest you to create new List with filtered data and use it in your adapter. Still if you want an alternate solution you can try below code:
#Override
public int getCount() {
int halfCountOfList = itemList.size()/2;
// Add +1 in halfCountOfList if itemList size is odd.
int finalCount = halfCountOfList + itemList.size()%2;
return finalCount;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
Item item = getItem(position*2);
//No need to to check for even item
return view;
}
No, just no. This is a really bad idea.
Don't do this. It makes no sense. Think about it: getView() is called because a View for an item is required. It is too late to make any decision about whether this item needs to be displayed in the ListView or not, it already has to be displayed. What you are trying to do is simply not how ListViews and Adapters are supposed to work.
Just filter the List before passing it to the Adapter, you can modify the List while it is being displayed by exposing it through a getter method on the Adapter and then immediately calling notifyDataSetChanged(). Any other solution which deviates from this or tries to make Adapters work like you expect it would just be a brittle hack and should never be used.
PS: Use a RecyclerView instead of a ListView. It's much better.
If you give a ListView adapter a set of data to display, it's going to try to call getView() on all of the data when it's needed - it doesn't discriminate. The decisions on how to display it are made in the getView() function.
The two are options are to either modify the data set, or only provide full-fledged views to your even index-ed values. Unfortunately, there aren't other ways around this.
Make a another list which contains only even position items in it and pass it on adapter. adapter will call getView method for all items present in list.
make another function in adapter
public int getItemViewType(int position) {
// return a value between 0 and (getViewTypeCount - 1)
return position % 2;
}
After that in GetView() method
int viewType = getItemViewType(position);
switch (viewType) {
case 0:
layoutResource = ;//Even Item
break;
case 1:
layoutResource = ;//Odd Item
break;
}
IF Your data will be of some objectType and the database will return
ArrayList<Object>,
then you can do this
Eg:-
ListView lv; // assign this view to some ListView
final List<Object> listResult; //sorted list returned by database
int len = listResult.size();
for (int i = 1; i <= len; i++) {
if (i % 2 == 0){
Object x = listResult.get(i);
//extract the data you need from the object returned
lv.add(x);
}
}
I want to implement search functionality for my RecyclerView. On text changed i want to change the data that are displayed with this widget. Maybe this question has been asked before or is simple, but I don't know how the change the data that is to be shown...
My RecyclerView is defined as follows:
// 1. get a reference to recyclerView
mRecyclerView = (RecyclerView)findViewById(R.id.recyclerView);
// 2. set layoutManger
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
// 3. create an adapter
mAdapter = new ItemsAdapter(itemsData);
// 4. set adapter
mRecyclerView.setAdapter(mAdapter);
And the data that I am showing is something like:
ItemData itemsData[] = { new ItemData("Mary Richards"),
new ItemData("Tom Brown"),
new ItemData("Lucy London")
};
So when when I want to give the adapter another set of data, another array (with one item for example), what should I do?
If you have stable ids in your adapter, you can get pretty good results (animations) if you create a new array containing the filtered items and call
recyclerView.swapAdapter(newAdapter, false);
Using swapAdapter hints RecyclerView that it can re-use view holders. (vs in setAdapter, it has to recycle all views and re-create because it does not know that the new adapter has the same ViewHolder set with the old adapter).
A better approach would be finding which items are removed and calling notifyItemRemoved(index). Don't forget to actually remove the item. This will let RecyclerView run predictive animations. Assuming you have an Adapter that internally uses an ArrayList, implementation would look like this:
// adapter code
final List<ItemData> mItems = new ArrayList(); //contains your items
public void filterOut(String filter) {
final int size = mItems.size();
for(int i = size - 1; i>= 0; i--) {
if (mItems.get(i).test(filter) == false) {
mItems.remove(i);
notifyItemRemoved(i);
}
}
}
It would perform even better if you can batch notifyItemRemoved calls and use notifyItemRangeRemoved instead. It would look sth like: (not tested)
public void filterOut(String filter) {
final int size = mItems.size();
int batchCount = 0; // continuous # of items that are being removed
for(int i = size - 1; i>= 0; i--) {
if (mItems.get(i).test(filter) == false) {
mItems.remove(i);
batchCount ++;
} else if (batchCount != 0) { // dispatch batch
notifyItemRangeRemoved(i + 1, batchCount);
batchCount = 0;
}
}
// notify for remaining
if (batchCount != 0) { // dispatch remaining
notifyItemRangeRemoved(0, batchCount);
}
}
You need to extend this code to add items that were previously filtered out but now should be visible (e.g. user deletes the filter query) but I think this one should give the basic idea.
Keep in mind that, each notify item call affects the ones after it (which is why I'm traversing the list from end to avoid it). Traversing from end also helps ArrayList's remove method performance (less items to shift).
For example, if you were traversing the list from the beginning and remove the first two items.
You should either call
notifyItemRangeRemoved(0, 2); // 2 items starting from index 0
or if you dispatch them one by one
notifyItemRemoved(0);
notifyItemRemoved(0);//because after the previous one is removed, this item is at position 0
This is my answer - thanks to Ivan Skoric from his site: http://blog.lovelyhq.com/creating-lists-with-recyclerview-in-android/
I created an extra method inside my adapter class:
public void updateList(List<Data> data) {
mData = data;
notifyDataSetChanged();
}
Then each time your data changes, you just call this method passing in your new data and your view should change to reflect it.
Just re-initialize your adapter:
mAdapter = new ItemsAdapter(newItemsData);
or if you only need to remove add a few specific items rather than a whole list:
mAdapter.notifyItemInserted(position);
or
mAdapter.notifyItemRemoved(position);
If you want to change the complete Adapter in the recycler view. you can just simply set by recycler.setAdapter(myAdapter);
It will automatically remove the old adapter from recycler view and replace it with your new adapter.
As ygit answered, swapAdapter is interesting when you have to change the whole content.
But, in my FlexibleAdapter, you can update the items with updateDataSet. You can even configure the adapter to call notifyDataSetChanged or having synchronization animations (enabled by default). That, because notifyDataSetChanged kills all the animations, but it's good to have for big lists.
Please have a look at the description, demoApp and Wiki pages: https://github.com/davideas/FlexibleAdapter