I have a list view fed by BaseAdapter.
When something changes in data, I call BaseAdapter.notifyDataSetChanged. All works fine.
Now I wonder, if I change some tiny detail in some list item, is there some optimized way to update view of single item if it's displayed on screen?
I suppose that notifyDataSetChanged blindly rebuilds views of all visible items in list, which is not optimal if trivial change in item happens.
No, I guess it's not possible unless you implement your own ListView class extension.
Here there is the source code for setAdapter().
You will see, the ListView only registers itself as observer using mAdapter.registerDataSetObserver(mDataSetObserver);
And a DataSetObserver provides no means to notify about a change at a certain position.
However it might not be necessary to notify about updates of certain items, because as far as I know, a ListView only renders and updates the items currently seen on the screen so, no optimization should be necessary here.
Yes, there is way. Assuming I can identify list item, for example by assigning View.setTag to it to some value, I can iterate list items and rebind only one list item if desired, or even update only some sub-view of the item.
It's simple and relatively cheap (linear search):
for(int i = list.getChildCount(); --i>=0; ){
View v = list.getChildAt(i);
Object id = v.getTag();
if(id==myId){
updateListItem(id, v);
break;
}
}
where myId is some ID of item I want to update, and updateListItem makes desired changes on the list item. Proved, and working fine and very efficiently.
Related
I have a ListView which I populate using a custom CursorAdapter.
Now I want to manually update just one specific item in the ListView. I have the content URI of that item. Is it possible to use just this info to get the position of the item in the listView?
If I have the position I can do something like
View v = mListView.getChildAt(itemPosition - mListView.getFirstVisiblePosition());
to update the view. But how can I get itemPosition?
I know that I get the position in the onItemClickListener, but I need to update the view without it being clicked.
Any help guys?
ListView does not support updating a single position.
You must update the adapter with the new data (even if you change a single position) and the, invoke mAdapter.notifyDataSetChanged()
If you get the View by its position (via listView.getChildAt()) will work. However, if you scroll up and down the list, that view will display the old data again because the adapter is not aware of the change (and it is the adapter which update the view contect view getView()).
When you invoke notifyDataSetChanged(), you are telling to the adapter that your data set has new info and the ListView/Adapter will re-draw the visible items (it won't re-draw whole list at once.. only the visible items).
You may want to consider to change to RecyclerView in the future. The BaseAdapter used in a RecyclerView support actions such as add/remove/update a single position.
I have been battling with RecyclerView for the last 2 days. Unlike any time in the past, I want to have swipe to dismiss functionality.
I followed
this guide
The only difference here that I have implemented is instead of using a predefined array for input, I have an API response to populate the data. All that seems to be good, when I open the recyclerview the items populate as they should. In the order that they should.
Here is where the fun begins. If I try to swipe to dismiss the first item I get an IndexOutOfBounds error. Thats cute, searching I found that some choose to set a new List based on the first list, remove the item and then re-assign the original list to the new list. notifyDataSetChanged() when done. No more error BUT no animation of the recycler view to bring the 2nd item to the top of the list. any time notifyDataSetChanged() is called the list does NOT move. I scroll down and back up and the item is back. Or so I thought.
After playing with this for a while I noticed that while the information in the first item appeared to the be the original first item, its got an ID of the second item in the list and the last item in the view is now missing.
Here is an example:
text = Fox id = a
text = Dog id = b
text = Pig id = c
I swipe dismiss 1 from the list, this is what the UI shows
text = Fox id = b
text = Dog id = c
Lots of code to embed here and its throwing errors trying to do so, gist is here https://gist.github.com/baggednismo/2840d0f777438d29673bc29096153970
Try removing setHasStableIds(true) and the overridden getItemId() from your adapter.
You're telling the RecyclerView that the nth view will always represent the same item in the list, and it's getting upset when you remove one of the items.
(I just tried a very simplified example with setHasStableIds(true) and got a similar exception; it worked fine when I removed it and the getItemId())
Note that your call to notifyItemDeleted() is preferable to replacing the entire list, but if you did want or need to replace the entire list, you should look at using DiffUtil to determine and make the appropriate changes to the list, and it will do all the cool animation if the same items are still present.
If you want to see my example, it's at https://gist.github.com/javadude/dec1862b075338790f023b42e01fd953 (but it's in kotlin)
#Scott Stanchfield was very much correct on his input. The noted changes in his comments and answer cleaned up the view to help me find the actual view related issue. The suggestions are still implemented and are part of the solution.
onBindViewHolder() created the view of items of which it was creating the line items based on the original API response order and not the list order recyclerview was handling. onBindViewHolder() is called each time a new item is to appear in the list on scroll and this was the problem. This works fine on the very first inflation of the list however once the item was removed and scrolling back to the top the first item appeared to be the item dismissed.
Original:
KitchenOrdersResponse.Item mItem = mKitchenOrdersResponse.getItems().get(i);
Resolution:
KitchenOrdersResponse.Item mItem = mItems.get(i);
Hi I have run into a design issue and can't figure out the best approach to take. Here is a sample of what I am trying to achieve:
When the user clicks on the Plus icon another row will be added below the current condition. Currently the condition row is in a listview and is setup using a custom adapter. The left col is filled with the options that the user has chosen from the select columns view. All that is working but I can't seem to figure out how to add a new row. So is there a way to dynamically add a new row to the list and fill it with the same data? OR is there a better alternate way to set this up?
So your custom adapter is starting off with three rows? Then when the user clicks on the plus icon, the onClick listener should call a method on your adapter that sets some kind of indication that there are now four rows (after which it calls notifyDataSetChanged()). And your getView() method should look at that indicator and if position == 2 (third row) and the indicator is not set, it returns an "Order By" view. If the indicator is set, then it should return a "Condition View" and only return an "Order By" view when position == 3 (fourth row). Does that make sense?
Your adapter reflects the state of your list model at any moment in time. Any event that occurs where you want to change the ListView should update some kind of state in the adapter. Then getView() should always check that current state when returning a view. So always think in two phases:
Your event (like onClick) updates some model data in the adapter somewhere.
Your adapter's getView() method always checks the current model data in the adapter to figure out how to set up the view for the given position.
I have a question about some behavior in an AdapterView.OnItemClickListener that has me a bit flummoxed.
I have a ListView backed by a custom CursorAdapter. The Cursor is being managed by a LoaderManager. The ListView also has an OnItemClickListener. The data loads properly and the ListView is correctly populated - in this case with many items.
What I'm finding is that the position argument to onItemClick is 0-based relative to the items showing on the screen, not the entire list. So for example if I scroll down and select the first viewable item, I'll actually get the first item in the list (which is not visible) instead of the one I selected. When I use a debugger, I see that position actually has a value of 0 even though the item selected was much further down in the list.
What's odd is that this worked fine until I started using the LoaderManager.
I'm using the v4 support library.
If I do something like this:
public void onItemClick( AdapterView<?> parent, View listItem, int position, long id ) {
Cursor c = myListAdapter.getItem( parent.getFirstVisiblePosition() + position );
// do stuff
Then I get the data I actually selected, but I somehow feel this is wrong. I shouldn't have to use getFirstVisiblePosition().
Any ideas?
Thanks
I guess you might be using the convertView parameter of the getView() method to recycle the views. Using this might be causing the issues here. Kindly try using an alternative and check whether the issue still exists or not
I would like to create a ListView in Android where I have the ability to add a new blank row, and have the controls in the new row be editable. Then on some event (either the user clicks add again, selects another row, or some other trigger I haven't determined yet), I want to update the Adapter with whatever values the user entered into the editable row.
I currently have editable controls within each row and the ability to add a blank row via a menu item. I cannot figure out how to sync the user entered data with the Adapter.
I originally thought that Adapters are two way data binds, but that doesn't seem to be the case. From my research and experimenting, if I change an Adapter value and call notifyDataSetChanged(), then the UI gets updated. Is there a reverse operation?
I was able to accomplish the two-way data binding by adding a KeyListener and OnFocusChangeListener to each of the controls on my row's View. Both of these events will call into a method I created on my row's View to loop through all the controls on the view and update my adapter's data with the current values. I had to make sure to not call notifyDataSetChanged(). This method is necessary only for programmatically changing the data source object and having the UI reflect the changes.
Not the most efficient way ever, but it works decently well.
Another thing to note, adding and deleting rows I needed to set both control and view level squelching of updating of my adapter view. For deletions, what I did was add a long click event on my row's View to have a menu with a delete option. Then I started squelching updates on a View level, because I programmatically edit my data source object to remove the given row data and call notifyDataSetChanged() (necessary otherwise OS will throw an exception). Squelching here makes sure I don't hit my events and get into an infinite loop and that my data is properly synced. Then on the deleted row View I set all my controls to squelch their event's update adapter. This is because the deleted row View will still have focus, and I want to make sure I don't update my data source object with values not on the UI. This flag gets flipped once I get the row's View back from the ListView recycling process in getView() of my adapter.
Adding a new row I also need to squelch on just the row's View level. This is because I programmatically change my data source with a new empty row of data, and call notifyDataSetChanged. Reasons are exactly the same as the delete.
your problem is hat you, for example may hav 300 items on the list (that are repesented by EditItems) but only 12-20 EditItems in reality that are recycled.
i guess your only way to know that use has finished with his row is FocsedChangedListener on each of your views.
Once focus is off use:
in your adapter's getView use: if v is your View then do v.setTag(position)
in the OnFocusChangedListener once focus is off use: int pos = (Integer)v.getTag(); mAdapter.updatePosition(text,pos)
make sure your adapter has an updat mthod that will update the object in position pos with the String 'text'
To refine C Nick's solution a bit, you can use EditText.addTextChangedListener.