Adapter / ArrayAdapter

Adapters – Part 3

In the previous article we got an ArrayAdapter and a slightly more complex SimpleAdapter working, but suggested that we could also achieve the same result as the SimpleAdapter using an ArrayAdapter. In this article we’ll look at how to do this, but also explore some performance issues which we need to be aware of when working with Adapters.

In principle, replacing the SimpleAdapter with an ArrayAdapter is relatively simple. We create a class to hold the data for each item:

private static class Item
{
	public final String line1;
	public final String line2;
		
	public Item(String line1, String line2)
	{
		this.line1 = line1;
		this.line2 = line2;
	}
}

We can then use an ArrayAdapter instead of the ArrayAdapter that we used previously. The only problem here is that ArrayAdapter has no implicit knowledge of our Item class, so we need to perform the actual data binding ourselves. We do this by creating our own ArrayAdapter implementation and overriding the getView() method:

public class ArrayAdapterObjectFragment 
	extends ListFragment
{
	private static final List items = 
		new ArrayList();

	private static class Item
	{
		public final String line1;
		public final String line2;
		
		public Item(String line1, String line2)
		{
			this.line1 = line1;
			this.line2 = line2;
		}
	}
		
	private class ItemAdapter extends ArrayAdapter
	{

		public ItemAdapter(Context context)
		{
			super(context, 
				android.R.layout.simple_list_item_2, 
				items);
		}
		
		@Override
		public View getView(int position, 
			View convertView, 
			ViewGroup parent)
		{
			/* This implementation is very 
			 * inefficient and should not be used
			 * as is.
			 */
			Item item = getItem(position);
			View view = LayoutInflater.from(
				getContext()).inflate(
					android.R.layout.simple_list_item_2, 
					parent, false);
			((TextView)view.findViewById(android.R.id.text1))
				.setText(item.line1);
			((TextView)view.findViewById(android.R.id.text2))
				.setText(item.line2);
			return view;
		}
	}
	
	static
	{
		items.add(new Item("Title One", "Subtitle One"));
		items.add(new Item("Title Two", "Subtitle Two"));
		items.add(new Item("Title Three", "Subtitle Three"));
		items.add(new Item("Title Four", "Subtitle Four"));
		items.add(new Item("Title Five", "Subtitle Five"));
	}
	
	@Override
	public View onCreateView(LayoutInflater inflater, 
		ViewGroup container, Bundle savedInstanceState)
	{
		return inflater.inflate(R.layout.listview, 
			container, false);
	}
	
	@Override
	public void onActivityCreated(Bundle savedInstanceState)
	{
		super.onActivityCreated(savedInstanceState);
		ListAdapter adapter = new ItemAdapter(getActivity());
		setListAdapter(adapter);
	}
}

Hopefully this should be fairly understandable, and it does precisely what we require:

ArrayAdapterObject

However, there is a problem with this, as you may be able to guess from the comment in the getView() implementation. To understand the problem, we have to consider how ListView works, particularly when we are scrolling through a list. Each time an item in the ListView comes in to view when scrolling, getView() will be called. Our getView() implementation is performing two quite expensive operations: layout inflation, and finding views within the layout.

Layout inflation is expensive because it requires the parsing of XML and the creation of a number of object instances representing the controls within the XML.

Finding views is expensive because it requires traversal of the view hierarchy in order to find the control that we’re interested in.

The net result of having these expensive operations within our getView() method is that the scrolling of our ListView will not be smooth. This will be compounded because the items going out of view will need to be garbage collected by the VM in order to free up the memory that will be necessary to create new objects during view inflation, and garbage collection is also an expensive operation.

Fortunately the Adapter framework gives us a mechanism for recycling our item layouts. When items go out of view, their layouts will be recycled and they will be passed in to our getView() method in the convertView argument. So we can perform a null check on this, and only inflate the view if we have to:

@Override
public View getView(int position, 
	View convertView, 
	ViewGroup parent)
{
	/* This implementation is better
	 * than before but still 
	 * inefficient and should not be used
	 * as is.
	 */
	Item item = getItem(position);
	View view = convertView;
        if(view == null)
	{
		LayoutInflater.from(
			getContext()).inflate(
			android.R.layout.simple_list_item_2, 
			parent, false);
	}
	((TextView)view.findViewById(android.R.id.text1))
		.setText(item.line1);
	((TextView)view.findViewById(android.R.id.text2))
		.setText(item.line2);
	return view;
}

This will optimise performance quite considerably but we’re still performing these expensive findViewById() calls each time getView() is called. Now that we are re-using our layouts, the actual TextView objects that we’re binding to are also being reused so we can cache them. We can do this by using a View Holder pattern where we create a new object containing references to our controls and attach this to the parent layout:

public static class ViewHolder 
{
	public final TextView text1;
	public final TextView text2;
		
	public ViewHolder(TextView text1, TextView text2)
	{
		this.text1 = text1;
		this.text2 = text2;
	}
}

@Override
public View getView(int position, 
	View convertView, 
	ViewGroup parent)
{
	View view = convertView;
	ViewHolder holder = null;
	if(view == null)
	{
		view = LayoutInflater.from(
			getContext()).inflate(
				android.R.layout.simple_list_item_2, 
				parent, false);
		TextView text1 = 
			(TextView)view.findViewById(android.R.id.text1);
		TextView text2 = 
			(TextView)view.findViewById(android.R.id.text2);
		view.setTag(new ViewHolder(text1, text2));
	}
	if(holder == null && view != null)
	{
		Object tag = view.getTag();
		if(tag instanceof ViewHolder)
		{
			holder = (ViewHolder)tag;
		}
	}
	Item item = getItem(position);
	if(item != null && holder != null)
	{
		holder.text1.setText(item.line1);
		holder.text2.setText(item.line2);
	}
	return view;
}

Now we only perform the layout inflation and findViewById() calls when we absolutely have to, thus keeping the code in getView() much more efficient. You should always avoid expensive calls such as filesystem and network operations in the getView() method.

In the next article we’ll have a look at how we can bind images to an ImageView within our item layout without making expensive calls in getView().

The source code for this article is available here.

© 2013 – 2014, Mark Allison. All rights reserved.

Copyright © 2013 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.