Refreshing ListView
If you’re like me, you’ve encountered situations where calling notifyDataSetChanged()
on your adapter doesn’t seem to do anything. Certainly, something is happening, but that doesn’t seem to always cause your ListView or ExpandableListView to redraw on screen to reflect changes to your underlying dataset. Once again, trusty StackOverflow reveals that there’s some controversy surrounding this issue.
Thankfully, the accepted answer is exactly what the Android team recommends. True to StackOverflow form however, the first comment on the accepted answer with more than a handful of votes, declares that notifyDataSetChanged()
doesn’t work! Further down another user points out the gotcha’s that a lot of developers run in to; all of which, are discussed in The World of ListView from GoogleIO 2010 that I mentioned in the introduction to this series. If you haven’t watched it yet, I can’t recommend enough that you view it now.
That should be the end of the story, right? Not quite. Even Mark Murphy, author of The Busy Coder’s Guide to Android Development and one of the foremost Android experts recently uncovered a bug in EndlessAdapter; one of his open source contributions which employs notifyDataSetChanged()
.
For us mere mortals in the Android community, the heart of this issue is almost certainly a simple misunderstanding on what notifyDataSetChanged()
and its companion notifyDataSetInvalidated()
are used for. They are used to reflect changes to (or complete invalidation of) the dataset your adapter is pulling from and should be called on the UI thread immediately after making changes to your data. If you’re curious about what exactly is going on behind the scenes after notifying your adapter, take a look at AbsListView’s AdapterDataSetObserver and its call to AdapterView’s (super) implementation.
Please Recycle
By now I’m going to assume you’ve got warm and fuzzies about notifyDataSetChanged()
and notifyDataSetInvalidated()
. Let’s talk about what to do when you’re not making changes to your dataset, but still need to update some visual aspect of your list. You could scrap your whole adapter and replace it with a new one that reflects the visual changes you want, but that’s hacky and not performant. Remember that on mobile, poor performance means eating up your user’s battery even if the user doesn’t notice visual sluggishness. Fortunately, there’s a better way. You’re probably painfully aware by now that ListView and ExpandableListView use a view recycler that drastically increases performance. It achieves this by scrapping views as they fall off the visible portion of the screen and reusing only the few it needs. Those views are then returned to your adapter as the convertView parameter in getView(int position, View convertView, ViewGroup parent)
. If you’re curious about the recycler implementation, I encourage you take a look at the source. There’s even a RecyclerListener if you’re so inclined. This recycling strategy means we can make the visual changes we want to only the views visible on the screen and then let getView take care of making sure our recycled views look the way we want. Let’s jump in to a simple coloring example.
Color Me not so Surprised
In the full example project I’ve implemented some EditTexts for users to input desired text and background colors for our ListView items as well as button to apply those colors. As most of it is boiler plate, I’ll just show you the relevant bits here. ExpandableListView has some important implementation differences which make it a bit more complicated, but the strategy is the same. Feel free to skip ahead to the ExpandableListView example if that’s more relevant to you.
ListView
Refreshing Visible Items
We’ll be using the provided getFirstVisiblePosition()
, getLastVisiblePosition()
and getChildAt(int position)
methods to grab the visible item views and make our changes. It’s important that you don’t make any structural changes to your item views. Doing so will create performance issues caused by remeasuring and conflicts with optimization assumptions ListView makes. Implement getItemViewType(position)
and getViewTypeCount()
instead for structural changes as demonstrated here.
// Declare the initial list item color variables
private int bgColor = Color.WHITE;
private int textColor = Color.BLACK;
/* Grabs the user color inputs and converts them proper integer color values.
* Since we're working on ListView, two inputs are needed; text color and
* background color.
*
* Definitely check out Color.parseColor(String). It's very handy!
*/
private void retrieveColors() {
if (mTextColorInput.getText().length() > 0)
textColor = Color.parseColor(mTextColorInput.getText()
.toString()
.toLowerCase());
if (mBackgroundColorInput.getText().length() > 0)
bgColor = Color.parseColor(mBackgroundColorInput.getText()
.toString()
.toLowerCase());
}
/* Applies the user supplied color values to the visible list items. This
* does not touch the recycled off screen views.
*/
private void applyColors() {
View listItem;
Holder mHolder;
retrieveColors(); //First we collect our user input and
//convert it to valid integer color values
// These are both implemented in AdapterView which means
// they're flat list positions
int firstVis = mList.getFirstVisiblePosition();
int lastVis = mList.getLastVisiblePosition();
/* This is the "conversion" from flat list positions to ViewGroup child positions */
int count = lastVis - firstVis;
while (count >= 0) { // looping through visible list items
/* getChildAt(pos) is implemented in ViewGroup and has a different meaning for
* its position values. ViewGroup tracks visible items as children and is 0 indexed.
* This means you'll have 0 - X positions where X is however many items it takes
* to fill the visible area of your screen; usually less than 10. */
listItem = mList.getChildAt(count);
if (listItem != null) {
mHolder = (Holder) listItem.getTag();
if (mHolder == null) { // This shouldn't happen, but we'll make
// sure in case some strange concurrency
// bugs appear
mHolder = new Holder();
mHolder.mText = (TextView) listItem.findViewById(android.R.id.text1);
listItem.setTag(mHolder);
}
listItem.setBackgroundColor(bgColor); // Setting our user input
// background color
mHolder.mText.setTextColor(textColor);// Setting our user input
// text color
} else
Log.d(TAG, "getChildAt retrieved a null view :( ");
count--;
}
}
Applying Changes to All Items
Now that we’ve got our visible item updates working, we need to make sure all of our items reflect our color changes. For this, we just need to override our adapter’s getView(int position, View convertView, ViewGroup parent)
method and ensure that each convertView we get shows the correct colors.
@Override
public View getView(int position, View convertView, ViewGroup parent){
Holder mHolder;
if (convertView == null){ // ViewHolder pattern implementation
convertView = getLayoutInflater().inflate(android.R.layout.simple_list_item_1,
parent,
false);
mHolder = new Holder();
mHolder.mText = (TextView) convertView.findViewById(android.R.id.text1);
convertView.setTag(mHolder);
}else
mHolder = (Holder) convertView.getTag();
/* Since all visible items have had their appropriate user selected
* colors applied, we need to make sure any of the recycler's
* scrapped views (which wouldn't be touched by our visible item
* changes) also reflect our changes. */
convertView.setBackgroundColor(bgColor);
mHolder.mText.setTextColor(textColor);
mHolder.mText.setText(getItem(position).toString());
return convertView;
}