Optimized ListView Having Listener within The Row Items

ListView is an extensively used widget in Android Applications for displaying data in a structured way. To display custom made views as row elements, it is always preferred to associate Base Adapter with ListView. This is how a BaseAdapter can be used to show a list in which each element contains a text field and a checkbox:

MainActivity.java

// Initialize the ListView and set its adapter.
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(new NormalListAdapter((Context) this));

NormalListAdapter.java

// Initialize the ListView and set its adapter.
	ListView listView = (ListView) findViewById(R.id.list_view);
	listView.setAdapter(new NormalListAdapter((Context) this));
	…

NormalListAdapter.java
	public class NormalListAdapter extends BaseAdapter {
	/**
	 * Context of the activity where associated list is present.
	 */
	private Context mContext;

	/**
	 * Data-Set of items that are inflated in every row.
	 */
	private List<ListItemPojo> mItems;

	/**
	 * Constructor of {@link NormalListAdapter}.
	 *
	 * @param context
	 *            Context of the activity where associated list is present.
	 */
	public NormalListAdapter(Context context) {
		mContext = context;

		// Initializing the item data.
		mItems = new ArrayList<ListItemPojo>();
		for (int i = 0; i < 30; i++) {
			ListItemPojo item = new ListItemPojo();
			item.setItemName("Item Number " + (i + 1));
			item.setItemCheck(false);
			mItems.add(item);
		}
	}

	/**
	 * @return Total number of data items.
	 */
	@Override
	public int getCount() {
		return (mItems == null) ? 0 : mItems.size();
	}

	/**
	 * @return Data item present at supplied position in DataSet.
	 */
	@Override
	public ListItemPojo getItem(int position) {
		return mItems.get(position);
	}

	/**
	 * @return Id of data item at supplied position.
	 */
	@Override
	public long getItemId(int position) {
		return position;
	}

	/**
	 * @return The view of row for supplied position, old (convertView) view can also be recycled.
	 */
	@Override
	public View getView(final int position, View convertView, ViewGroup parent) {
		// ---------> Here we are not reusing any old view and re-make every time getView() is called.
		convertView = LayoutInflater.from(mContext).inflate(R.layout.item_list, null);
		CheckBox itemCheck = (CheckBox) convertView.findViewById(R.id.item_checkbox);
		TextView itemName = (TextView) convertView.findViewById(R.id.item_name);

		// ---------> Set the values that are saved in the DataSet.
		ListItemPojo currentItem = getItem(position);
		itemName.setText(currentItem.getItemName());
		itemCheck.setChecked(currentItem.isItemCheck());
		itemCheck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
			@Override
			public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
				// Change the CheckStatus in DataSet when CheckBox is clicked.
				mItems.get(position).setItemCheck(isChecked);
			}
		});
		return convertView;
	}
}

Here, we are just creating a new view everytime getView() is called to show the view of row item that just entered the screen, Nowadays most high end phones and tablets are equipped with at least a dual core processor, which can handle un-optimized code at tolerable speeds. However, a large percentage of Android devices are still using single core processors with limited memory. We aim to deliver one form of optimization that developers can place in their code. Here’s how we can optimize the resource usage by re-using the view of element going out of the screen for the view that is entering the screen:

/**
	 * @return The view of row for supplied position, old (convertView) view can also be recycled.
	 */
	@Override
	public View getView(final int position, View convertView, ViewGroup parent) {
		/*
		 * ---------> Inflate a new view if old view is null and set a Tag for it, else get the tag and use
		 * it.
		 */
		ListItemHolder itemHolder = null;
		if (convertView == null) {
			convertView = LayoutInflater.from(mContext).inflate(R.layout.item_list, null);
			itemHolder = new ListItemHolder();
			itemHolder.mItemName = (TextView) convertView.findViewById(R.id.item_name);
			itemHolder.mItemCheck = (CheckBox) convertView.findViewById(R.id.item_checkbox);
			convertView.setTag(itemHolder);
		} else {
			itemHolder = (ListItemHolder) convertView.getTag();
		}

		// ---------> Set the values that are saved in the DataSet.
		ListItemPojo currentItem = getItem(position);
		itemHolder.mItemName.setText(currentItem.getItemName());
		itemHolder.mItemCheck.setChecked(currentItem.isItemCheck());
		itemHolder.mItemCheck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
			@Override
			public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
				// Change the CheckStatus in DataSet when CheckBox is clicked.
				mItems.get(position).setItemCheck(isChecked);
			}
		});
		return convertView;
	}

 

Here we are converting the view of exiting element and re-using it for entering element. But, this is very tricky approach specially while using the listeners that can be invoked implicitly, for e.g. OnCheckChangedListener, OnFocusListener, TextWatcher, etc. as they can be invoked without user interaction.

Here’s how you can avoid being trapped in this tricky approach while using listeners for views within the row:

The approach is just to remove the listener from the view before setting its data or focusing it so that the listener which is already associated with the view for old element doesn’t gets invoked on its own.

/**
	 * @return The view of row for supplied position, old (convertView) view can also be recycled.
	 */
	@Override
	public View getView(final int position, View convertView, ViewGroup parent) {
		/*
		 * ---------> Inflate a new view if old view is null and set a Tag for it, else get the tag and use
		 * it.
		 */
		ListItemHolder itemHolder = null;
		if (convertView == null) {
			convertView = LayoutInflater.from(mContext).inflate(R.layout.item_list, null);
			itemHolder = new ListItemHolder();
			itemHolder.mItemName = (TextView) convertView.findViewById(R.id.item_name);
			itemHolder.mItemCheck = (CheckBox) convertView.findViewById(R.id.item_checkbox);
			convertView.setTag(itemHolder);
		} else {
			itemHolder = (ListItemHolder) convertView.getTag();
		}

		// ---------> Set the values that are saved in the DataSet.
		ListItemPojo currentItem = getItem(position);
		itemHolder.mItemName.setText(currentItem.getItemName());
		// ---------> Here before setting the value of any view for which we are going to register a listener,
		// first set the listener as null so that the listener for old view doesn't get invoked implicitly
		// which can accidently change the value for view as older position also.
		itemHolder.mItemCheck.setOnCheckedChangeListener(null);
		itemHolder.mItemCheck.setChecked(currentItem.isItemCheck());
		itemHolder.mItemCheck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
			@Override
			public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
				// Change the CheckStatus in DataSet when CheckBox is clicked.
				mItems.get(position).setItemCheck(isChecked);
			}
		});
		return convertView;
	}

To understand this approach with a working example you can download the code from here.
Note: I have also used ActionBarCompat, so to compile the code you need to import this library.

 

Written By: Ankit Bansal, Android Developer, Mindfire Solutions

Advertisements

4 thoughts on “Optimized ListView Having Listener within The Row Items

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s