Android – Working with ListViews
Listview is a viewgroup used to display a list of scrollable items. List items are populated from a data source, such as an array or database query, using an adapter and displayed into a view that’s placed into the list.
Using listviews is very commun in mobile applications development. This tutorial is an introduction to the basics of manipulating a listview and to the different types of lists and adapters in Android. There’s no better place to get you started with Android’s listviews.
The listView widget
Android provides two classes capable of displaying a scrollable list of items : ListView and ExpandableListView. The expandableListView supports groupings of items.
Input types for listviews are obviously Java objects (unless you need to try populating a Java list with potatoes or apples). You need to define your data (java objects), and the views (default or customised list row views) to hold it inside the list row. This happens with the help of the adapter which extracts the correct data from the data object and assigns it to the views.
Adapters
As their name indicates, adapaters adapt ‘your content to your layout’. Adapters are not only used by ListView
, but also by other views which extend AdapterView as, for example, Spinner, GridView, Gallery and StackView. An adapter manages the data model and adapts it to the individual entries in the widget. It extends the BaseAdapter class.
Every row in your list consists of a layout which can be as complex as you want. The adapter, using it’s getView() method, inflates the layout for each row then assigns the appropriate data for each layout.
A typical listView row would contain an image and some text as follows :
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="?android:attr/listPreferredItemHeight" android:padding="6dip" > <ImageView android:id="@+id/icon" android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_alignParentBottom="true" android:layout_alignParentTop="true" android:layout_marginRight="6dip" android:src="@drawable/ic_launcher" /> <TextView android:id="@+id/secondLine" android:layout_width="fill_parent" android:layout_height="26dip" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_toRightOf="@id/icon" android:ellipsize="marquee" android:singleLine="true" android:textSize="12sp" /> <TextView android:id="@+id/firstLine" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_above="@id/secondLine" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:layout_alignWithParentIfMissing="true" android:layout_toRightOf="@id/icon" android:gravity="center_vertical" android:textSize="16sp" /> </RelativeLayout>
So once your list row layout is ready, you have to work on the adapter to popluate the imageviews and textviews with data for each row.
Default Adapters:
The most used default adapters are ArrayAdapter and CursorAdapter. ArrayAdapter can handle data based on Arrays or java.util.List. CursorAdapter can handle database related data.
The ArrayAdapter class can handle a list or arrays of Java objects as input. Every Java object is mapped to one row. By default, it maps the toString()
method of the object to a view in the row layout.
The ArrayAdapter
class allows to remove all elements in its underlying data structure with the clear()
method call. You can then add new elements via the add()
method or a Collection
via the addAll()
method. You can change the data in your adapter calling the notifyDataSetChanger() method. Notice that the underlying data structure must support this operation.
This is a basic way to set an ArrayAdapter for a listview:
public class ListViewExampleActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_listviewexampleactivity); final ListView listview = (ListView) findViewById(R.id.listview); String[] values = new String[] {"Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2", "Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2", "Android", "iOS", "WindowsPhone" }; ArrayAdapter adapter = new ArrayAdapter(this,android.R.layout.simple_list_item_1,values); listview.setAdapter(adapter); } }
Custom Adapters:
The ArrayAdapter is limited as it supports only the mapping of toString to one view in the row layout. But what if we need more than one view in the same row ? As in the exapmle of the custom list row mentionned above. Then we need to implement a custom adapter.
For this we would extend an existing adapter implementation or subclass the BaseAdapter class directly.
First step is to create a layout for each row of the list. The ListView instance calls the getView() method on the adapter for each data element. In this method the adapter creates the row layout and maps the data to the views in the layout.
After the adapter inflated the layout, it searches for the relevant views in the layout and fills them with the data.
This is a basic example for a custom adapter implementation:
public class MySimpleArrayAdapter extends ArrayAdapter { private final Context context; private final String[] values; public MySimpleArrayAdapter(Context context, String[] values) { super(context, -1, 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 icon for mobile os String s = values[position]; if (s.startsWith("iOS") || s.startsWith("Android") || s.startsWith("WindowsPhone") ) { imageView.setImageResource(R.drawable.mobileOS); } else { imageView.setImageResource(R.drawable.os); } return rowView; } }
ListActivity and ListFragment
The ListActivity and ListFragment classes are made for when you need to use lists in activities or in fragments. You dont have to define a layout for those classes if you’ll use them. They contain a single listview by default.
ListActivity and ListFragment also allow you to override the onListItemClick() method for handling selection of list items. Both classes allow you to set the adapter to the default listView via the setListAdapter() method.
The following example code shows a basic ListFragment implementation:
public class MyListFragment extends ListFragment { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); String[] values = new String[] {"Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2" }; ArrayAdapter adapter = new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_1, values); setListAdapter(adapter); } @Override public void onListItemClick(ListView l, View v, int position, long id) { // when a listItem is clicked } }
For the ui associated, you have to define a listView with the predefined android:id attribute set to @android:id/list as follows :
<ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="wrap_content" > </ListView>
If you do not use this ID or do not include a ListView into your layout, the application crushes once you try to display the activity or the fragment.
Listviews and performance
ListViews are designed for scalability and performance, they try to do as few view inflations as possible and they only paint and lay out children that are (or are about to become) visible on screen. The reason for doing few view inflations is that those are expensive operations: they involve going through a tree of special XML blocks and instantiating all respective views. The listView class solves this problem by recycling “non-visible views”. This means that developers can simply update the contents of recycled views instead of inflating the layout of every single row.
• In order to only paint or lay out children that are visible on screen, lisviews use the view recycler to keep adding recycled views below or above the current viewport and moving active views to a recyclable pool as they move off-screen while scrolling. This way ListView only needs to keep enough views in memory to fill its allocated space in the layout and some additional recyclable views—even when your adapter has hundreds of items.
The following image visually summarizes what happens when you scroll a listview:
Inflating layouts and creating Java objects (as every view which gets inflated from an XML layout file will result in a Java object) are expensive with regards to time and memory consumption. In addition using the findViewById() method is relatively time consuming.
ListViews call the adapter’s getView() method very frequently when scrolling, which means findViewById() is being called, and that might perceivably hit scrolling performance in ListViews.
• The View Holder pattern is about reducing the number of findViewById() calls in the adapter’s getView(). In practice, the View Holder is a lightweight inner class that holds direct references to all inner views from a row. You store it as a tag in the row’s view after inflating it. This way you’ll only have to use findViewById() when you first create the layout. Here’s the previous code sample with View Holder pattern applied:
public class MyPerformanceArrayAdapter extends ArrayAdapter { private final Activity context; private final String[] names; static class ViewHolder { public TextView text; public ImageView image; } public MyPerformanceArrayAdapter(Activity context, String[] names) { super(context, R.layout.rowlayout, names); this.context = context; this.names = names; } @Override public View getView(int position, View convertView, ViewGroup parent) { View rowView = convertView; // reuse views if (rowView == null) { LayoutInflater inflater = context.getLayoutInflater(); rowView = inflater.inflate(R.layout.rowlayout, null); // configure view holder ViewHolder viewHolder = new ViewHolder(); viewHolder.text = (TextView) rowView.findViewById(R.id.TextView01); viewHolder.image = (ImageView) rowView .findViewById(R.id.ImageView01); rowView.setTag(viewHolder); } // fill data ViewHolder holder = (ViewHolder) rowView.getTag(); String s = names[position]; holder.text.setText(s); if (s.startsWith("WindowsPhone") || s.startsWith("iOS") || s.startsWith("Android")) { holder.image.setImageResource(R.drawable.mobileOs); } else { holder.image.setImageResource(R.drawable.os); } return rowView; } }
• Asyncronous loading is another option that improves listview’s performance: Using drawable resources in your adapter’s getView() is usually fine as Android caches those internally. But you might want to show more dynamic content, coming from local disk or internet. In that case, you probably don’t want to load them directly in your adapter’s getView() because, well, you should never ever block UI thread with IO. What you need to do is run all per-row IO or any heavy CPU-bound routine asynchronously in a separate thread. The trick here is to do that and still comply with ListView‘s recycling behaviour.
public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = new ViewHolder(); View rowView = convertView; ... if (rowView == null) { LayoutInflater inflater = context.getLayoutInflater(); rowView = inflater.inflate(R.layout.rowlayout, null); // configure view holder viewHolder.text = (TextView) rowView.findViewById(R.id.TextView01); rowView.setTag(viewHolder); } ViewHolder holder = (ViewHolder) rowView.getTag(); String s = names[position]; holder.text.setText(s); new ThumbnailTask(holder) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null); return convertView; } private static class ThumbnailTask extends AsyncTask { private ViewHolder mHolder; public ThumbnailTask(ViewHolder holder) { mHolder = holder; } @Override protected Cursor doInBackground(Void... arg0) { // Download bitmap here } @Override protected void onPostExecute(Bitmap bitmap) { mHolder.thumbnail.setImageBitmap(bitmap); } }
But if you blindly start an asynchronous operation on every getView() call while scrolling, you’d be wasting a lot of resources as most of the results would be discarded due to rows being recycled very often.
• You need to add interaction awareness to your ListView adapter so that it doesn’t trigger any asynchronous operation per row after scrolling the list so fast that it doesn’t make sense to even start any asynchronous operation. Once scrolling stops, or is about to stop, is when you want to start actually showing the heavy content for each row.
You can also balance interaction awareness with an in-memory cache so that you show cached content even while scrolling.
That’s all coder ! As I already mentionned there’s no better place to get you started with Android’s listviews ! Coming Android tutorials will cover more listviews topics .
Recent Comments