Android Adventures - ListView And Adapters
Written by Mike James   
Thursday, 08 October 2015
Article Index
Android Adventures - ListView And Adapters
Interacting with lists
Custom ArrayAdapter
Improving Efficiency, Summary
Custom Adapter - Complete Listing

Reuse, Caching and General Layouts

We have a working custom adapter class but there are some things we can do to make it better. 

The first relates to efficiency. 

If you recall it was pointed out that a big list of objects could result in the creation of a lot of View objects. In practice however we really only need the number of View object that correspond to rows actually being displayed on the screen. 

To avoid having to dispose of and create new View objects all the time the ListView gives you the opportunity to recycle the View objects you have already created. This is what the convertView parameter in the getView method is all about. If it is null you have to inflate and create a new View object. If it is non-null then it is a View object ready to be used and you don't have to create a new one. 

Modifying the previous example to make use of convertView is easy:

View row;
if(convertView==null) {
 LayoutInflater inflater =
  ((Activity) context).getLayoutInflater();
  row = inflater.inflate(resource, parent, false);
 }else{
  row=convertView;
 }

This is a speedup worth making and saves having to create lots of View objects and disposing of them.

However we are still looking up the child View objects every time we want to change the data. That is:

TextView title= (TextView)
            row.findViewById(R.id.title);
TextView number=(TextView)
            row.findViewById(R.id.number);

This is also very wasteful and can be avoided with the application of the ViewHolder pattern. 

All we have to do is save the references to the children in an object and store this in the parent View's tag property. Then the next time we see the parent View we don't have to find the children we are looking for - they are stored in the parent's tag as a ViewHolder object.

First we need to create a ViewHolder object:

private static class ViewHolder {
 TextView title;
 TextView number;
}

Notice that this has fields capable of holding references to our two TextView objects.

The logic of the getView method is now to also create and store a ViewHolder object if we are not recycling a View object

View row;
ViewHolder viewHolder;
if (convertView == null) {
 viewHolder = new ViewHolder();
 LayoutInflater inflater =
        ((Activity) context).getLayoutInflater();
 row = inflater.inflate(
         resource, parent, false);
 viewHolder.title =
        (TextView) row.findViewById(R.id.title);
 viewHolder.number =
        (TextView) row.findViewById(R.id.number);
 row.setTag(viewHolder);
}

Notice that we have stored the references to the TextViews in the viewHolder and stored this in the row's Tag field - this is what tag fields genreally are used for. 

If we do have a recycled View object then we just need to get the viewHolder object: 

} else {
 row = convertView;
 viewHolder = (ViewHolder) convertView.getTag();
}

Finally no matter where the viewHolder object came from we just use it:

 viewHolder.title.setText(
      (CharSequence) objects[position].myTitle);
 viewHolder.number.setText(
      Integer.toString(objects[position].myNum));
 return row;
}

With this change we have avoided creating a View object each time and we have avoided having to lookup the child objects each time - very useful savings in time and resources.

Finally there is one last polish to apply. At the moment the layout for the row has to have the ids of the TextView objects set to title and number. Much better to let the user set these in the constructor:

public MyAdapter(Context context,
                     int resource,
                     int resTitle,
                     int resNumber,
                     MyData[] objects) {
 super(context, resource, objects);
 this.context = context;
 this.resource = resource;
 this.textRes1= resTitle;
 this.resNumber = resNumber;
 this.objects = objects;
}

 

This constructor has two extra parameters used to specify the id numbers of the two ViewText objects in the layout. These are stored in private variables for later use - and we need to declare the private variables:

private int resTitle;
private int resNumber;

The original constructor can still be retained as long as it sets resTitle and resNumber:

public MyAdapter(Context context,
                 int resource,
                 MyData[] objects) {
 super(context, resource, objects);
 this.context = context;
 this.resource = resource;
 this.objects = objects;
 this.resTitle =R.id.title;
 this.resNumber =R.id.number;
}

 

Finally we need to change the getView method to use the two new private variables:

viewHolder.title =
         (TextView) row.findViewById(resTitle);
viewHolder.number =
         (TextView) row.findViewById(resNumber);

 

With these changes the adapter can be used as:

MyAdapter myAdapter=new MyAdapter(
  this,
  R.layout.mylayout,
  R.id.title,
  R.id.number,
  myDataArray);

and the user is free to use any ids for the layout as long are there are two TextViews.

Summary

  • Containers like ListView work together with an adapter to display data.

  • The adapter accepts a data structure - usually an array or a list - and converts any data item into a View object that represents the data.

  • You can handle a selection event to find out what the user has selected.

  • The View object can be complex with an outer container and any number of child objects.

  • The basic ArrayAdapter uses each object's toString method to provide the data to display in a ViewText object.

  • If you want to display something other than the result of calling toString you need to implement a custom ArrayAdapter.

  • To do this you have to override the inherited getView method.

  • The ListView is clever enough to provide you with View objects to recycle - although you don't have to if you don't want to.

  • The ViewHolder pattern can make your use of nested View objects more efficient.

What Is Missing?

As with all things, there are aspects of the ListView that haven't been covered. In particular, what do you do if your data structure isn't an array?

The ArrayAdapter can handle Lists, simply override the appropriate constructor. Anything more complex and you will have to create a custom Adapter. In most cases, however, this isn't necessary because most data structures can be mapped onto an array.

There are a range of formatting topics not covered - the header and separator for example - but these are relatively easy. 

More complicated are the multiselection options and any custom list displays such as a list with multiple columns or pop-out detail windows. 

 



Last Updated ( Saturday, 15 October 2016 )