2012/07/10

Android:HeaderViewListAdapterでOutOfBoundsException


過去に出会った不具合について覚書。

Header付きListViewを持つDialogを表示しながら画面回転したり、
言語切替した後に復帰すると稀にエラーが発生しました。

●手順

  1. Header付きListViewを持つDialogを表示中に画面回転
または
  1. Header付きListViewを持つDialogを表示
  2. Homeボタンでアプリをバックグラウンドへ
  3. 言語切り替えを行う
  4. 手順1の画面に復帰

●試験結果

アプリが強制終了する場合がある。

エラーログ:
FATAL EXCEPTION: main
java.lang.IndexOutOfBoundsException: Invalid index 0, size is 0
  at java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:251)
  at java.util.ArrayList.get(ArrayList.java:304)
  at android.widget.HeaderViewListAdapter.isEnabled(HeaderViewListAdapter.java:164)
  at android.widget.ListView.dispatchDraw(ListView.java:3342)
  at android.view.View.draw(View.java:10999)
  at android.widget.AbsListView.draw(AbsListView.java:3591)
  at android.view.View.getDisplayList(View.java:10435)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.HardwareRenderer$GlRenderer.draw(HardwareRenderer.java:875)
  at android.view.ViewRootImpl.draw(ViewRootImpl.java:2027)
  at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1751)
  at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2559)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:137)
  at android.app.ActivityThread.main(ActivityThread.java:4475)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:511)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:792)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:559)
  at dalvik.system.NativeStart.main(Native Method)

●再現率

3/5

●原因解析

同様の現象が発生する最小構成のコードが下記。
public class HeaderListViewActivity extends Activity {
    ArrayList<String> elements = new ArrayList<String>();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        elements.clear();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU) {
            showDialog(1);
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        if (id == 1) {
            elements.add("A");
            elements.add("B");
            elements.add("C");
            ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                    android.R.layout.simple_list_item_1, elements);
            AlertDialog dialog = new AlertDialog.Builder(this)
                    .setSingleChoiceItems(adapter, 1, null).create();

            TextView header = new TextView(this);
            header.setText("header");
            dialog.getListView().addHeaderView(header, null, false);

            return dialog;
        }

        return super.onCreateDialog(id);
    }
}

問題が発生するトリガはonDestroyで実行しているelements.clear()でした。
elementsはDialogが持つListViewに紐付けられたデータセットです。
タイミングによっては、onDestroyの後にDialogのListViewがelementsへアクセスする
ようで、HeaderViewListAdapter.isEnabledで範囲外を参照してしまうようです。

また、この例外は37行目のaddHeaderViewで、第三引数にfalseを渡した場合に発生します。
# つまりtrueを設定すると再現しない

●解決策

今回のケースだと、elementsをclearする理由がなかったので、これをやめるだけで対応
できたけれど、clearする必要がある場合はどうするのだろう。。。

stackoverflowをみると「Samsung phneだと起きる」という人がいたりするけれど、
今回の結果を見ると、組む側の問題のような気がします...

# 個人的にListHeaderViewやListFooterView周辺はよく不具合が出るので、
# なるべくなら使用を避けたいところです。

以上です。