Tutorial: ListView in Android using custom ListAdapter and Cache View

The aim of this article is to build a custom ListView for Android using a custom ListAdapter and a View Cache.
In Android is very simple to build a list view until you don’t want change the row layout.
Indeed, the customization of a ListView is very powerful, but it needs a bit of attention to optimize it and to make everything working in a proper way.
But if you follow some simple practices, also putting a custom layout for the each row can became easy. ;)

The final result of this tutorial is:

custom list view android

Here we want to build a list view to store information about a list of cities, in particular each row has to contain: an image of the city, the name and the related wikipedia link.
So we start creating the city’s object:

package com.framentos.list;

public class City {

    private String name;
    private String urlWiki;
    private String image;

    public City(String name, String urlWiki, String image) {
        super();
        this.name = name;
        this.   urlWiki = urlWiki;
        this.image = image;
    }

    public String getName() {
        return name;       
    }

    public void setName(String nameText) {
        name = nameText;
    }

    public String getUrlWiki() {
        return urlWiki;
    }

    public void setUrlWiki(String urlWiki) {
        this.urlWiki = urlWiki;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }
}

The second step is to create the layout that we want put in each row of our list view:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="fill_parent"
   android:layout_height="80dip" >

    <ImageView
       android:id="@+id/ImageCity"
       android:layout_width="90sp"
       android:layout_height="90sp" />

    <LinearLayout
       android:layout_width="fill_parent"
       android:layout_height="fill_parent"
       android:layout_toRightOf="@id/ImageCity"
       android:orientation="vertical"
       android:paddingLeft="10sp">

        <TextView
           android:id="@+id/cityName"
           android:layout_width="fill_parent"
           android:layout_height="wrap_content"
           android:textSize="25sp" />

        <TextView
           android:id="@+id/cityLinkWiki"
           android:layout_width="fill_parent"
           android:layout_height="wrap_content"
           android:autoLink="web"
           android:textSize="15sp" />
    </LinearLayout>

</RelativeLayout>

Now we have to create the custom Adapter. the following code shows a “simple” Adapter (without optimization ):

package com.framentos.list;
import java.util.List;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class CityListAdapter extends ArrayAdapter{

    private int resource;
    private LayoutInflater inflater;
    private Context context;

    public CityListAdapter ( Context ctx, int resourceId, Listobjects) {

        super( ctx, resourceId, objects );
        resource = resourceId;
        inflater = LayoutInflater.from( ctx );
    context=ctx;
    }

    @Override
    public View getView ( int position, View convertView, ViewGroup parent ) {

        /* create a new view of my layout and inflate it in the row */
        convertView = ( RelativeLayout ) inflater.inflate( resource, null );

        /* Extract the city's object to show */
        City city = getItem( position );

        /* Take the TextView from layout and set the city's name */
        TextView txtName = (TextView) convertView.findViewById(R.id.cityName);
        txtName.setText(city.getName());

        /* Take the TextView from layout and set the city's wiki link */
        TextView txtWiki = (TextView) convertView.findViewById(R.id.cityLinkWiki);
        txtWiki.setText(city.getUrlWiki());

        /* Take the ImageView from layout and set the city's image */
        ImageView imageCity = (ImageView) convertView.findViewById(R.id.ImageCity);
        String uri = "drawable/" + city.getImage();
        int imageResource = context.getResources().getIdentifier(uri, null, context.getPackageName());
        Drawable image = context.getResources().getDrawable(imageResource);
        imageCity.setImageDrawable(image);
        return convertView;
    }
}

The code is quite simple. In the constructor we save the custom layout (resourceId), create an object “LayoutInflater” (we will use it to create a view of our layout) and save the context (we will need it just to access the resources).
Then we create method “getView”. Android will call it everytime it needs to render a row. In this method we:

1) create a new View by LayoutInflater and put it into the row’s view( convertView ).
2) take the number of the row ( position ) which has to be rendered and extract the correspondent city from the array.
3) find every graphic component from our view and set it with information.

Now the main activity which puts all together and its relative layout:

package com.framentos.list;
import java.util.ArrayList;
import java.util.List;
import com.framentos.list.R;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.AdapterView.OnItemClickListener;

public class CityList extends Activity {

    private ListView listViewCity;
    private Context ctx;
   
    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.city_list);
        ctx=this;
        List listCity= new ArrayList();
        listCity.add(new City("London","http://en.wikipedia.org/wiki/London","london"));
        listCity.add(new City("Rome","http://en.wikipedia.org/wiki/Rome","rome"));
        listCity.add(new City("Paris","http://en.wikipedia.org/wiki/Paris","paris"));

        listViewCity = ( ListView ) findViewById( R.id.city_list);
        listViewCity.setAdapter( new CityListAdapter(ctx, R.layout.city_row_item, listCity ) );
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent"
   android:orientation="vertical" >

    <ListView
       android:id="@+id/city_list"
       android:layout_width="fill_parent"
       android:layout_height="fill_parent"
       android:paddingTop="20sp" >
    </ListView>
   
</LinearLayout>

Our Adapter works in a proper way until we don’t populate our list with a lot of rows. If we do it we will see that the scrolling of the list will not be fluent.
This is due to the fact that for the rendering of each row we create a new view and we take every graphic component from it (a lot findViewById).
These operations are quite costly in terms of memory and cpu. To avoid it we can edit our adapter to use a Cache View.
The code of Cache View will be:

package com.framentos.list;

import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

class CityListViewCache {

    private View baseView;
    private TextView textNameCity;
    private TextView textWikiCity;
    private ImageView imageCity;

    public CityListViewCache ( View baseView ) {
        this.baseView = baseView;
    }

    public View getViewBase ( ) {
        return baseView;
    }

    public TextView getTextNameCity (int resource) {
        if ( textNameCity == null ) {
            textNameCity = ( TextView ) baseView.findViewById(R.id.cityName);
        }
        return textNameCity;
    }

    public TextView getTextWikiCity (int resource) {
        if ( textWikiCity == null ) {
            textWikiCity = ( TextView ) baseView.findViewById(R.id.cityLinkWiki);
        }
        return textWikiCity;
    }

    public ImageView getImageView (int resource) {
        if ( imageCity == null ) {
            imageCity = ( ImageView ) baseView.findViewById(R.id.ImageCity);
        }
        return imageCity;
    }
}

And we edit our Adapter to use it:

import java.util.List;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class CityListAdapterWithCache extends ArrayAdapter{

    private int resource;
    private LayoutInflater inflater;
    private Context context;

    public CityListAdapterWithCache ( Context ctx, int resourceId, Listobjects) {
        super( ctx, resourceId, objects );
        resource = resourceId;
        inflater = LayoutInflater.from( ctx );
        context=ctx;
    }

    @Override
    public View getView ( int position, View convertView, ViewGroup parent ) {

        City city = getItem( position );
        CityListViewCache viewCache;

        if ( convertView == null ) {
            convertView = ( RelativeLayout ) inflater.inflate( resource, null );
            viewCache = new CityListViewCache( convertView );
            convertView.setTag( viewCache );
        }
        else {
            convertView = ( RelativeLayout ) convertView;
            viewCache = ( CityListViewCache ) convertView.getTag();
        }

        TextView txtName = viewCache.getTextNameCity(resource);
        txtName.setText(city.getName());

        TextView txtWiki = viewCache.getTextWikiCity(resource);
        txtWiki.setText(city.getUrlWiki());

        ImageView imageCity = viewCache.getImageView(resource);
        String uri = "drawable/" + city.getImage();
        int imageResource = context.getResources().getIdentifier(uri, null, context.getPackageName());
        Drawable image = context.getResources().getDrawable(imageResource);
        imageCity.setImageDrawable(image);
        return convertView;
    }
}

In this case, when the system wants to render a row, it uses the cache class to take every component from our layout (getTextNameCity, getTextWikiCity, getImageView).
In this way our ListView will be fine to contain many rows. ;)

Last note it’s to remind you that every interaction between the user and the rows will be lost if they aren’t saved.
Android renders the rows which have to be showed. When they disappear from screen they are destroyed (the system attempts to keep a few of them until he doesn’t have memory problem).
For example, we have a checkbox in each row and the user clicks in a particular row and changes its status (from off to on) and after he scrolls the listview.
If after the user goes back on that row he will see that the status of the checkbox is off. So pay attention to create a way to save the interaction between user and your listView.

You can download the complete project here

android-tutorial ,

18 responses to Tutorial: ListView in Android using custom ListAdapter and Cache View


  1. kwozmo kramer

    tnx for this very helpful tutorial.

    i’m now trying to make an app that displays another activity containing the city’s info upon clicking on an item, instead of the wiki…

    my problem is i can’t capture the click event of the items on the list…can you provide some help on how to do this…

    i’ve tried onListItemClick and onClick but they’re not working…

    • You should add in the mail activity listViewCity.setItemsCanFocus(true);, so it becomes:

      ....
      listViewCity = ( ListView ) findViewById( R.id.city_list);
      listViewCity.setAdapter( new CityListAdapter(ctx, R.layout.city_row_item, listCity ) );
      listViewCity.setItemsCanFocus(true);

      and now you can put the listener for each element in the Adapter (in getView) , for example:

      ...
      final OnClickListener lsImageView = new OnClickListener() {
          @Override
              public void onClick(View v) {
                 Log.d("CITYLIST","You have clicked in the ImaveView");
          }
      };
      imageCity.setOnClickListener(lsImageView);

      You remember listViewCity.setItemsCanFocus(true);. If you don’t put it, the listeners don’t work.
      I hope it work for you.
      Bye!

  2. simayi

    Thanks so much ! But I don’t understand about different between “dip” and “sp” !
    Why do you uses it for layout ?

  3. Vu Hung

    Thank you very much, this tutorial is useful for me

  4. suresh

    Really good , i am going to use it now

  5. Akshat Sharma

    hi i made the app the same way as you did in the above tutorial. but when i run the app on emulator it says unfortunately , stop working. Can you help me out with query.

  6. Michael

    Thanks for the great tutorial. Is there a way to add and remove items from the listView after the fact? I’m not sure what to do after calling the .add function.

  7. luke729

    Very good post. I’m dealing with some of these issues as well..

  8. this was i am looking for. Thanks bro

  9. streax74

    very good

  10. Jay

    Thank you very much.! this helped me a lot.! very easy to understand.

  11. Vignesh

    How to get selected list item name on click?

  12. Dorey

    nice article !

  13. Hi,

    It seem’s like you have made a mistake.

    public CityListAdapter ( Context ctx, int resourceId, Listobjects) {

    should be public CityListAdapter ( Context ctx, int resourceId, List Listobjects) {

    Otherwise, it seem’s good :)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>