ListView + CheckBoxでチェック可能なリストを作るときの注意点

こんな一見簡単そうなものを実装しようとしてハマったメモ。

まず、自前でArrayAdapterを継承してアダプタを作る事になると思うんですが、
getViewをオーバーライドして、各行のViewが要求されたときにチェックボックスを含むViewを返してやるだけ…だとだめなことがあります。

具体的に言うと、以下の第2引数のconvertViewを使い回してposition用のViewとしてreturnするような場合です。

public View getView(int position, View convertView, ViewGroup parent)

convertView != nullの場合、その中身は「どこかのpositionの表示のためにgetViewでreturnされたView」です。
必ずしも、いまgetViewが呼ばれてるpositionで使われたViewではないので、returnする前にチェック状態をリセットしないといけない。
それをしないと、

「あるチェックボックスをチェックしたら、表示範囲外のチェックボックスが何故か一緒にチェックされてた。何を言っているかわからないと思うが(ry」ということになります。

このあたりに対処できるコードを置いておきます。
特に BooleanListAdapter#getViewあたり。
参考になれば幸いです。

public class MultipleChoiceListActivity extends ListActivity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       
        setListAdapter(new BooleanListAdapter(this, R.layout.list_row1, buildTestData()));
    }
    
    protected List<Boolean> buildTestData() {
        List<Boolean> items = new ArrayList<Boolean>();
        items.add(true);
        items.add(false);
        items.add(true);
        items.add(false);
        items.add(true);
        items.add(false);
        items.add(true);
        items.add(false);
        items.add(true);
        items.add(false);
        items.add(true);
        items.add(false);
        items.add(true);
        items.add(false);

        return items;
    }
    
    class BooleanListAdapter extends ArrayAdapter<Boolean> {
    	protected LayoutInflater inflater;
		private List<Boolean> items;
		private int rowLayoutResourceId;
		
    	public BooleanListAdapter(Context context, int rowLayoutResourceId, List<Boolean> items) {
    		super(context, rowLayoutResourceId, items);
    		this.items = items;
    		this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    		this.rowLayoutResourceId = rowLayoutResourceId;
    	}
    	
    	@Override
    	public View getView(int position, View convertView, ViewGroup parent) {
    		View view = convertView;
    		if (view == null) {
    			Log.i("MultipleChoiceListActivity", "constructing a new view for " + String.valueOf(position) + "-th list row");
    			view = inflater.inflate(R.layout.list_row1, null);
    		} else {
       			Log.i("MultipleChoiceListActivity", "reusing the view for " + String.valueOf(position) + "-th list row");
    		}
    		
    		CheckBox checkBox = (CheckBox) view.findViewById(R.id.CheckBox01);
    		Log.i("MultipleChoiceListActivity", "CheckBox#setChecked(" + String.valueOf(items.get(position)) + ")");
    		// 明示的にクリアするならこうだけど、とにかくsetChecked前にリスナ登録すればOK
    		// checkBox.setOnCheckedChangeListener(null);
    		final int p = position;
    		// 必ずsetChecked前にリスナを登録(convertView != null の場合は既に別行用のリスナが登録されている!)
    		checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

				@Override
				public void onCheckedChanged(CompoundButton buttonView,
						boolean isChecked) {
					Log.i("MultipleChoiceListActivity", "p=" + String.valueOf(p) + ", isChecked=" + String.valueOf(isChecked));
					items.set(p, isChecked);
				}
    		});
    		checkBox.setChecked(items.get(position));
    		
    		return view;
    	}
    }
}