Tuesday, 29 November 2011

Android ListView and ListActivity


1. Android and Lists

1.1. ListView

The display of Lists is a very common pattern in mobile applications. The user gets a list of items can scroll through them and select one item. This selection is used to trigger something else.
Android provides the view "ListView" which is capable of displaying a scrollable list of items.

1.2. ListActivity

You can directly use the "ListView" in your layout as any other UI component. In case your Activity is primary showing a list you can extend the activity "ListActivity" which simplifies the handling of a "ListView".
"ListActivity" extends "Activity" and provides simplified handling of lists. For example you have a predefine method if someone clicks on a list element.
"ListActivity" contains a "ListAdapter" which is responsible for managing the data. This adapter must be set in the onCreate() method of your Activity via the method setListAdapter().
If the user select in the list a list entry the method onListItemClick() will be called. This method allows to access the selected element.
Android provides already some default layouts which you can use in your Adapter, e.g. "android.R.layout.simple_list_item1". In case you don't want to use one of the pre-defined layouts your own layout must have an element with the id "@android:id/list" which is the ListView. This is defined in XML via the id attribute on the ListView. For example:

<ListView
  android:id="@+android:id/list"
  android:layout_width="match_parent"
  android:layout_height="wrap_content" >
</ListView>
   

You can also use a view with the id "@android:id/empty". This view is displayed if the list is empty. For example you could display here an error message.

2. Adapter

2.1. What are Adapters?

"ListView" gets the data to display via an adapter. An adapter which must extend "BaseAdapter" and is responsible for providing the data model for the list and for converting the data into the fields of the list.
Android has two standard adapters, ArrayAdapter and CursorAdapter . "ArrayAdapter" can handle data based on Arrays or Lists while "SimpleCursorAdapter" handle database related data. You can develop your own Adapter by extending these classes or the BaseAdapter class.
The most important method of the Adapter is getView(). getView() is called for every line in the ListView to determine which data should be displayed in the line and how. getView() also provides a parameter "convertView" which allow to re-use an existing row of the table which is not displayed anymore because the user scrolled it out of the visible part of the list. If this convertView is not null it can be re-used, e.g. you can only update the content of the row and you don't have to load the layout for the view which is a big performance saver as handling of XML files is an expensive operation.

2.2. ListViews and performance

Displaying a large dataset must be efficiently implemented on a mobile device. Therefore the ListView only creates views (widget) if needed and attach them to the view hierarchy. The default Adapter implementation for a ListView will recycle views, e.g. if a row is not displayed anymore it will be recycled and only its content will change. If you implement your own adapter for a view you also should do this to avoid performance problems.

3. Tutorial: Simple ListActivity

In this chapter we will create an extremely simple list based on some default provides by Android. We use the default Adapter class "ArrayAdapter" and a layout which is predefined by Android.
Create a new Android project "de.vogella.android.listactivity" with the activity "MyListActivity". You do not need to change the default layout "main.xml". Create the following activity.

package de.vogella.android.listactivity;

import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

public class MyListActivity extends ListActivity {
 public void onCreate(Bundle icicle) {
  super.onCreate(icicle);
  String[] values = new String[] { "Android", "iPhone", "WindowsMobile",
    "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
    "Linux", "OS/2" };
  ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
    android.R.layout.simple_list_item_1, values);
  setListAdapter(adapter);
 }

 @Override
 protected void onListItemClick(ListView l, View v, int position, long id) {
  String item = (String) getListAdapter().getItem(position);
  Toast.makeText(this, item + " selected", Toast.LENGTH_LONG).show();
 }
}
  

Note that we do not use the setContentView(). An ListActivity has per default a ListView available if you do not specify a layout which should be used.

ListActivity shows the Items

4. Tutorial: ListActivity with own layout

You can also define your own layout for the rows and assign this layout to your adapter. If you do this the layout will be the same for every entry, we will later learn how to be more flexible. In our example we will add a fixed icon to each list entry.
Create the following layout file "rowlayout.xml" in the res/layout folder of your project "de.vogella.android.listactivity".

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

    <ImageView
        android:id="@+id/icon"
        android:layout_width="22px"
        android:layout_height="22px"
        android:layout_marginLeft="4px"
        android:layout_marginRight="10px"
        android:layout_marginTop="4px"
        android:src="@drawable/ic_launcher" >
    </ImageView>

    <TextView
        android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@+id/label"
        android:textSize="20px" >
    </TextView>

</LinearLayout>
  

Change your activity to the following. This is almost the same coding as in the previous example, the only difference is that we are using our own layout in the ArrayAdapter and telling the adapter which UI element should contains the text. This was not necessary in the previous example a standard layout was used. If the text id is not provides Android searches for an element with the id @android:id/text1 in the layout of the row.

package de.vogella.android.listactivity;

import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

public class MyListActivity extends ListActivity {
 public void onCreate(Bundle icicle) {
  super.onCreate(icicle);
  String[] values = new String[] { "Android", "iPhone", "WindowsMobile",
    "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
    "Linux", "OS/2" };
  // Use your own layout
  ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
    R.layout.rowlayout, R.id.label, values);
  setListAdapter(adapter);
 }

 @Override
 protected void onListItemClick(ListView l, View v, int position, long id) {
  String item = (String) getListAdapter().getItem(position);
  Toast.makeText(this, item + " selected", Toast.LENGTH_LONG).show();
 }
}
  


5. Tutorial: ListActivities with flexible layout

5.1. Overview

The above example uses one layout for all rows. If you want to influence the display of the different rows you can define your own adapter and override the getView() method.
This method is responsible for creating the individual rows of your "ListView". getView() returns a View. This view is typically a Layout (ViewGroup) and contains several other views, e.g. an ImageView and a TextView. Within getView() you also set the values of the individual views.
To read an layout from an XML resource in getView() you can use the system service LayoutInflator.
In this example we extend ArrayAdapter but we could also directly implement "BaseAdapter"

5.2. Defining a simple Adapter

The not performance optimized implementation of an own Adapter is straightforward. Receive in your constructor the Activity and the data you want to display and save them to a field. In your getView() method you inflate your pre-defined layout for the rows and receive the relevant elements via findViewById() on the layout. You can then set their properties.
The following uses two images "no.png" and "ok.png". I placed it in the "res/drawable-mdpi" folder. You must create your own icons. In case you don't find any icons just copy "icon.png" and use a drawing program to change it a little bit.
Create the class "MySimpleArrayAdapter" which will serve as our adapter.

package de.vogella.android.listactivity;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class MySimpleArrayAdapter extends ArrayAdapter<String> {
 private final Context context;
 private final String[] values;

 public MySimpleArrayAdapter(Context context, String[] values) {
  super(context, R.layout.rowlayout, values);
  this.context = context;
  this.values = values;
 }

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  LayoutInflater inflater = (LayoutInflater) context
    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  View rowView = inflater.inflate(R.layout.rowlayout, parent, false);
  TextView textView = (TextView) rowView.findViewById(R.id.label);
  ImageView imageView = (ImageView) rowView.findViewById(R.id.icon);
  textView.setText(values[position]);
  // Change the icon for Windows and iPhone
  String s = values[position];
  if (s.startsWith("Windows7") || s.startsWith("iPhone")
    || s.startsWith("Solaris")) {
   imageView.setImageResource(R.drawable.no);
  } else {
   imageView.setImageResource(R.drawable.ok);
  }

  return rowView;
 }
}

   

To use this adapter, change the class "MyList" to the following.

package de.vogella.android.listactivity;

import android.app.ListActivity;
import android.os.Bundle;

public class MyListActivity extends ListActivity {
 public void onCreate(Bundle icicle) {
  super.onCreate(icicle);
  String[] values = new String[] { "Android", "iPhone", "WindowsMobile",
    "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
    "Linux", "OS/2" };
  MySimpleArrayAdapter adapter = new MySimpleArrayAdapter(this, values);
  setListAdapter(adapter);
 }

}
   

If you run this example you should get a list with different icons for the certain elements.

5.3. Performance Optimization for your own Adapter

Creating Java objects for each row is time and memory consumption. As described earlier Android recycles rows (views) of your list which are not displayed anymore and passes them to the method getView() as convertView parameter
Your adapter implementation can re-use this view and can avoid inflating a layout for this row. This saves memory and CPU consumption.
In your implementation you have to check if convertView is not null and if not then you can re-use it by setting the new data into the existing layout.
Our implementation will also use the so-called "ViewHolder" pattern. The method findViewById() is a expensive operation, therefore we should avoid doing this operation if not necessary.
The ViewHolder stores a reference to the required views in a row. This ViewHolder is then attached to the row via the method setTag(). Every view can get a tag assigned. If the row is recycled we can get the ViewHolder via getTag() method. This seems like a lot of overhead but is much faster then the repetitive call of findViewById().
Both techniques (re-used of existing views and the ViewHolder pattern) improve the performance of a ListView by approx. 175%, especially for larger data sets.
We still using the project "de.vogella.android.listactivity". Create the following class "MyArrayAdapter.java".

package de.vogella.android.listactivity;

import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class MyArrayAdapter extends ArrayAdapter<String> {
 private final Activity context;
 private final String[] names;

 public MyArrayAdapter(Activity context, String[] names) {
  super(context, R.layout.rowlayout, names);
  this.context = context;
  this.names = names;
 }

 // static to save the reference to the outer class and to avoid access to
 // any members of the containing class
 static class ViewHolder {
  public ImageView imageView;
  public TextView textView;
 }

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  // ViewHolder will buffer the assess to the individual fields of the row
  // layout

  ViewHolder holder;
  // Recycle existing view if passed as parameter
  // This will save memory and time on Android
  // This only works if the base layout for all classes are the same
  View rowView = convertView;
  if (rowView == null) {
   LayoutInflater inflater = context.getLayoutInflater();
   rowView = inflater.inflate(R.layout.rowlayout, null, true);
   holder = new ViewHolder();
   holder.textView = (TextView) rowView.findViewById(R.id.label);
   holder.imageView = (ImageView) rowView.findViewById(R.id.icon);
   rowView.setTag(holder);
  } else {
   holder = (ViewHolder) rowView.getTag();
  }

  holder.textView.setText(names[position]);
  // Change the icon for Windows and iPhone
  String s = names[position];
  if (s.startsWith("Windows7") || s.startsWith("iPhone")
    || s.startsWith("Solaris")) {

   holder.imageView.setImageResource(R.drawable.no);
  } else {
   holder.imageView.setImageResource(R.drawable.ok);
  }

  return rowView;
 }
}

   


package de.vogella.android.listactivity;

import android.app.ListActivity;
import android.os.Bundle;

public class MyListActivity extends ListActivity {
 public void onCreate(Bundle icicle) {
  super.onCreate(icicle);
  String[] values = new String[] { "Android", "iPhone", "WindowsMobile",
    "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X",
    "Linux", "OS/2" };
  setListAdapter(new MyArrayAdapter(this, values));
 }

}
   


6. Advanced ListActivities

6.1. Adding a longclick listener to the list items

You can also add a LongItemClickListener to the view. For this receive the ListView via the method getListVIew() and set the long click listener via the method setOnItemLongClickListener().

package de.vogella.android.listactivity;

import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

public class MyList extends ListActivity {

 
/** Called when the activity is first created. */
public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone", "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; ArrayAdapter<String> adapter = new TwoLayoutsArrayAdapter(this, names); setListAdapter(adapter); ListView list = getListView(); list.setOnItemLongClickListener(new OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MyList.this, "Item in position " + position + " clicked", Toast.LENGTH_LONG).show(); // Return true to consume the click event. In this case the // onListItemClick listener is not called anymore. return true; } }); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); // Get the item that was clicked Object o = this.getListAdapter().getItem(position); String keyword = o.toString(); Toast.makeText(this, "You selected: " + keyword, Toast.LENGTH_SHORT) .show(); } }

6.2.  Rows interacting with the data model

Your row layout can also contain views which interact with the underlying data model. For example you can have a "Checkbox" view in your row and if the checkbox is selected you change the data which is displayed in the row.
We still use the same project. Create a new row layout "rowbuttonlayout.xml"

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent" android:layout_height="wrap_content">
 <TextView android:text="@+id/label" android:layout_width="wrap_content"
  android:layout_height="wrap_content" android:id="@+id/label"
  android:textSize="30px"></TextView>
 <CheckBox android:id="@+id/check" android:layout_width="wrap_content"
  android:layout_height="wrap_content" android:layout_marginLeft="4px"
  android:layout_marginRight="10px" android:layout_alignParentRight="true"
  ></CheckBox>
</RelativeLayout>

   

create for this example the class "Model" which hold the name and the information if this element is currently selected.

package de.vogella.android.listactivity;

public class Model {

 private String name;
 private boolean selected;

 public Model(String name) {
  this.name = name;
  selected = false;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public boolean isSelected() {
  return selected;
 }

 public void setSelected(boolean selected) {
  this.selected = selected;
 }

}

   

Create the following Adapter. This adapter will add a listener on the Checkbox. If the checkbox is selected the underlying data of the model is also changed. Search Checkbox gets its model element assigned via the setTag() method.

package de.vogella.android.listactivity;

import java.util.List;

import android.app.Activity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;

public class InteractiveArrayAdapter extends ArrayAdapter<Model> {

 private final List<Model> list;
 private final Activity context;

 public InteractiveArrayAdapter(Activity context, List<Model> list) {
  super(context, R.layout.rowbuttonlayout, list);
  this.context = context;
  this.list = list;
 }

 static class ViewHolder {
  protected TextView text;
  protected CheckBox checkbox;
 }

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
  View view = null;
  if (convertView == null) {
   LayoutInflater inflator = context.getLayoutInflater();
   view = inflator.inflate(R.layout.rowbuttonlayout, null);
   final ViewHolder viewHolder = new ViewHolder();
   viewHolder.text = (TextView) view.findViewById(R.id.label);
   viewHolder.checkbox = (CheckBox) view.findViewById(R.id.check);
   viewHolder.checkbox
     .setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {

      @Override
      public void onCheckedChanged(CompoundButton buttonView,
        boolean isChecked) {
       Model element = (Model) viewHolder.checkbox
         .getTag();
       element.setSelected(buttonView.isChecked());

      }
     });
   view.setTag(viewHolder);
   viewHolder.checkbox.setTag(list.get(position));
  } else {
   view = convertView;
   ((ViewHolder) view.getTag()).checkbox.setTag(list.get(position));
  }
  ViewHolder holder = (ViewHolder) view.getTag();
  holder.text.setText(list.get(position).getName());
  holder.checkbox.setChecked(list.get(position).isSelected());
  return view;
 }
}

   

Finally change your "ListView" to the following.

package de.vogella.android.listactivity;

import java.util.ArrayList;
import java.util.List;

import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;

public class MyList extends ListActivity {

 
/** Called when the activity is first created. */
public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity ArrayAdapter<Model> adapter = new InteractiveArrayAdapter(this, getModel()); setListAdapter(adapter); } private List<Model> getModel() { List<Model> list = new ArrayList<Model>(); list.add(get("Linux")); list.add(get("Windows7")); list.add(get("Suse")); list.add(get("Eclipse")); list.add(get("Ubuntu")); list.add(get("Solaris")); list.add(get("Android")); list.add(get("iPhone")); // Initially select one of the items list.get(1).setSelected(true); return list; } private Model get(String s) { return new Model(s); } }

If you start your app you should be able to flag items. These changes will be reflected in your model.

7. Single vrs. Multiselection

You can also support single and multi selection. See the following snippets for examples. To get the selected item(s) use listView.getCheckedItemPosition() or listView.getCheckedItemPositions(). If you have stable ID you could also use listView.getCheckedItemIds() to get the selected ids.

package de.vogella.android.listactivity;

import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MyList extends ListActivity {

 
/** Called when the activity is first created. */
public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone", "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_multiple_choice, android.R.id.text1, names)); ListView listView = getListView(); listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); } }


package de.vogella.android.listactivity;

import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MyList extends ListActivity {

 
/** Called when the activity is first created. */
public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone", "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_single_choice, android.R.id.text1, names)); ListView listView = getListView(); listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); } }

8. Header and Footer

You can of course put arbitray elements around your ListView. For example you can define a layout with two TextViews and a ListView between them. If you do this, you must assign the id "@android:id/list" to the ListView, as the ListActivity searches for a view with this id. If you do this then one TextView will always be visible above the List (header) and the other will be visible below the ListView. If you want to display the header / footer view only if see the beginning / end of the list you can use view.setHeaderView() or view.setFooterView(). For example:

package de.vogella.android.listactivity;

import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class MyList extends ListActivity {

 
/** Called when the activity is first created. */
public void onCreate(Bundle icicle) { super.onCreate(icicle); // Create an array of Strings, that will be put to our ListActivity String[] names = new String[] { "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone", "Linux", "Windows7", "Eclipse", "Suse", "Ubuntu", "Solaris", "Android", "iPhone" }; View header = getLayoutInflater().inflate(R.layout.header, null); View footer = getLayoutInflater().inflate(R.layout.footer, null); ListView listView = getListView(); listView.addHeaderView(header); listView.addFooterView(footer); setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_single_choice, android.R.id.text1, names)); } }

9. SimpleCursorAdapter

In case you work with a content provider or directly with the database you can use the SimpleCursorAdapter to define the data for your ListView. For more information on Android and SQLight please see the SQLite and Lists Tutorial .
Create a new Android project "de.vogella.android.listactivity.cursor" with the activity "MyListActivity". Create the following activity.

package de.vogella.android.listactivity.cursor;

import android.app.ListActivity;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.ListAdapter;
import android.widget.SimpleCursorAdapter;

public class MyListActivity extends ListActivity {
 
/** Called when the activity is first created. */
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Cursor mCursor = getContacts(); startManagingCursor(mCursor); // Now create a new list adapter bound to the cursor. // SimpleListAdapter is designed for binding to a Cursor. ListAdapter adapter = new SimpleCursorAdapter(this, // Context. android.R.layout.two_line_list_item, // Specify the row template // to use (here, two // columns bound to the // two retrieved cursor // rows). mCursor, // Pass in the cursor to bind to. // Array of cursor columns to bind to. new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME }, // Parallel array of which template objects to bind to those // columns. new int[] { android.R.id.text1, android.R.id.text2 }); // Bind to our new adapter. setListAdapter(adapter); } private Cursor getContacts() { // Run query Uri uri = ContactsContract.Contacts.CONTENT_URI; String[] projection = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME }; String selection = ContactsContract.Contacts.IN_VISIBLE_GROUP + " = '" + ("1") + "'"; String[] selectionArgs = null; String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; return managedQuery(uri, projection, selection, selectionArgs, sortOrder); } }

Make sure you give your application the permission to read the contacts. (Uses Permissions "android.permission.READ_CONTACTS" in AndroidManifest.xml)