Logo Icon Logo
A Crowd-sourced Cookbook on Writing Great Android® Apps
GitHub logo Twitter logo OReilly Book Cover Art

Writing a Custom List Adapter

Author: Alex Leffelman
Published? true
FormatLanguage: WikiFormat

Problem:

Lists are ubiquitous throughout mobile applications. Android provides a simple and powerful interface to make exactly the list you need. This recipe explains the steps for customizing the content of a ListView.

Solution:

In the Activity that will host your ListView, we will define a private class that extends Android's BaseAdapter class. We will override the base class's methods to display custom views that you define in an XML layout file.

Discussion:

It's no secret that the best way to explain something is through an example, so let's dive in. This is code lifted out of a media application I wrote that allowed the user to build playlists from the songs on their SD card. As promised, we'll be extending the BaseAdapter class inside of my MediaListActivty:

private class MediaAdapter extends BaseAdapter {
...
}

Querying the phone for the media info is outside the scope of this recipe, but the data to populate the list was stored in a MediaItem class that kept standard Artist, Title, Album, and Track Number information, as well as a boolean field indicating if the item was selected for the current playlist. In certain cases you may want to continually add items to your list - for example, if you're downloading information and displaying it as it comes in - but for this purpose we're going to supply all the required data to the Adapter at once in the constructor.

public MediaAdapter(ArrayList<MediaItem> items) {
    mMediaList = items;
    ...
}

Now, if you're developing in Eclipse you'll notice that it wants us to override BaseAdapter's abstract methods. Let's take a look at those.

public int getCount() {
    return mMediaList.size();
}

The framework needs to know how many Views it needs to create in your list. It finds out by asking your Adapter how many items you're managing. In our case we'll have a View for every item in the media list.

public Object getItem(int position) {
    return mMediaList.get(position);
}
public long getItemId(int position) {
    return position;
}

We won't really be using these methods, but for completeness: getItem(int) is what gets returned when the ListView hosting this adapter calls getItemAtPosition(int), which won't happen in our case. getItemId(int) is what gets passed to the ListView.onListItemClick(ListView, View, int, int) callback when you select an item. It gives you the position of the view in the list and the ID supplied by your adapter. In our case they're the same.

The real work of your custom adapter will be done in the getView() method. This method is called every time the ListView brings a new item into view. When an item goes out of view, it is recycled by the system to be used later. This is a powerful mechanism for providing potentially thousands of View objects to our ListView while using only as many Views as can be displayed on the screen. The getView() method provides the position of the item it is creating, a View that may be not-null which the system is recycling for you to use, and the ViewGroup parent. You'll return either a new View for the list to display, or a modified copy of the supplied convertView parameter to conserve system resources. Let's look at the code:

public View getView(int position, View convertView, ViewGroup parent) {
    View V = convertView;

    if(V == null) {
        LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        V = vi.inflate(R.layout.media_row, null);
    }

    MediaItem mi = mMediaList.get(position);
    ImageView icon = (ImageView)V.findViewById(R.id.media_image);
    TextView title = (TextView)V.findViewById(R.id.media_title);
    TextView artist = (TextView)V.findViewById(R.id.media_artist);

    if(mi.isSelected()) {
        icon.setImageResource(R.drawable.item_selected);
    }
    else {
        icon.setImageResource(R.drawable.item_unselected);
    }

    title.setText(mi.getTitle());
    artist.setText("by " + mi.getArtist());

    return V;
}

We start by checking if we'll be recycling a View (which is good practice), or if we need to generate a new View from scratch. If we weren't given a convertView, we'll call the LayoutInflater service to build a View that we've defined in an XML layout file.

Using the View which we've ensured was built with our desired layout resource (or is a recycled copy of one we previously built), it's simply a matter of updating its UI elements. In our case we want to display the song title, the artist, and an indication of whether or not the song is in our current playlist. (I've removed the error-checking, but it's good practice to make sure any UI elements you're updating are not null - you don't want to crash the whole ListView if there was a small mistake in one item) This method gets called for every (visible) item in the ListView, so in this example we have a list of identical View objects with different data being displayed in each one. If you wanted to get really creative, you could populate the list with different view layouts based on its position or content.

That takes care of the required BaseAdapter overrides. However, you can add any functionality to your Adapter to work on the data set it represents. In my example, I want the user to be able to click a list item and toggle it on/off for the current playlist. This is easily accomplished with a simple callback on the ListView and a short function in the Adapter:

(This function belongs to ListActivity)

protected void onListItemClick(ListView l, View v, int position, long id) {
    super.onListItemClick(l, v, position, id);
		
    mAdapter.toggleItem(position);
}

(This is a member function in our MediaAdapter)

public void toggleItem(int position) {
    MediaItem mi = mMediaList.get(position);

    mi.setSelected(!mi.getSelected());
    mMediaList.set(position, mi);
			
    this.notifyDataSetChanged();
}

First we simply register a callback for when the user clicks an item in our list. We're given the ListView, the View, the position, and the ID of the item that was clicked, but we'll only need the position, which we simply pass to the MediaAdapter.toggleItem(int) method. In that method we update the state of the corresponding MediaItem and make an important call to notifyDataSetChanged(). This method lets the framework know that it needs to redraw the ListView. If we don't call it, we can do whatever we want to the data, but we won't see anything change until the next redraw (for example, when we scroll the list).

When it's all said and done, we need to tell the parent ListView to use our Adapter to populate the list. That's done with a simple call in the ListActivity's onCreate(Bundle) method:

MediaAdapter mAdapter = new MediaAdapter(getSongsFromSD());
this.setListAdapter(mAdapter);

First we instantiate a new Adapter with data generated from a private function that queries the phone for the song data, then we tell the ListActivity to use that adapter to draw the list. And there it is - your own list adapter with a custom view and extensible functionality.

No records found.