読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

Android SDK の動かないコード(中級編) ListView内の要素にアクセスしようとするとNullPointerExceptionで落ちるエラー

Android


以下のAndroidアプリのコードが意図した動作をしないのは,なぜですか。
(制限時間1分)


やりたい事:

  • リストビューを表示し,その先頭の要素にフォーカスする。


アクティビティ側のコード:

package com.example.activity.hoge;

import com.example.R;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class CodeTestActivity extends Activity {

    // リスト
    private ListView lv = null;

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

        // 列挙したい項目
        String[] items_for_lv = {
            "A",
            "B",
            "C"
        };

        // 組み込み済みの行レイアウトを使ってアダプタを生成
        ArrayAdapter<String> adapter_for_lv = new ArrayAdapter<String>(
            this,
            android.R.layout.simple_list_item_1,
            items_for_lv
        );

        // リストにアダプタをセット
        lv = (ListView)findViewById(R.id.listview1);
        lv.setAdapter(adapter_for_lv);

        // リストの先頭要素にフォーカス
        View tr_view = lv.getChildAt(0);
        tr_view.requestFocus();
    }
}

なおレイアウトのmain.xmlには,listview1 というidのListViewが記述されている。




発生する問題


アプリを起動した瞬間,例外が発生して落ちる。


LogCatのエラーログ:

DEBUG/AndroidRuntime(3821): Shutting down VM
WARN/dalvikvm(3821): threadid=3: thread exiting with uncaught exception (group=0x40033160)
ERROR/AndroidRuntime(3821): Uncaught handler: thread main exiting due to uncaught exception
ERROR/AndroidRuntime(3821): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example/com.example.activity.hoge.CodeTestActivity}: java.lang.NullPointerException
ERROR/AndroidRuntime(3821): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2503)
ERROR/AndroidRuntime(3821): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2519)
ERROR/AndroidRuntime(3821): at android.app.ActivityThread.access$2200(ActivityThread.java:123)
ERROR/AndroidRuntime(3821): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1870)
ERROR/AndroidRuntime(3821): at android.os.Handler.dispatchMessage(Handler.java:99)
ERROR/AndroidRuntime(3821): at android.os.Looper.loop(Looper.java:123)
ERROR/AndroidRuntime(3821): at android.app.ActivityThread.main(ActivityThread.java:4370)
ERROR/AndroidRuntime(3821): at java.lang.reflect.Method.invokeNative(Native Method)
ERROR/AndroidRuntime(3821): at java.lang.reflect.Method.invoke(Method.java:521)
ERROR/AndroidRuntime(3821): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
ERROR/AndroidRuntime(3821): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
ERROR/AndroidRuntime(3821): at dalvik.system.NativeStart.main(Native Method)
ERROR/AndroidRuntime(3821): Caused by: java.lang.NullPointerException
ERROR/AndroidRuntime(3821): at com.example.activity.hoge.CodeTestActivity.onCreate(CodeTestActivity.java:41)
ERROR/AndroidRuntime(3821): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
ERROR/AndroidRuntime(3821): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2466)
ERROR/AndroidRuntime(3821): ... 11 more
ERROR/SemcCheckin(3821): Get crash dump level : java.io.FileNotFoundException: /data/semc-checkin/crashdump


リストの先頭要素にアクセスしたタイミングで,null が返ってきている。

そのため,nullに対してメソッドを呼び出そうとしてNullPointerExceptionになる。


検証用のコード

この問題を調査する。


以下の2つのタイミングで,状況を確認したい。

  • (タイミング1)リストビューにアダプタ経由で要素をセットした直後。
  • (タイミング2)画面の描画が終わり,少ししてからボタンが押された時。


知りたいのは下記の2点。

  • リストビューの要素数。
  • リストビューの先頭要素の実体。

下記のようなコードで確認できる。

package com.example.activity.hoge;

import com.example.R;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;

public class CodeTestSampleActivity extends Activity implements OnClickListener {

    // リスト
    private ListView lv = null;
    private TextView tv = null;
    private Button btn = null;

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

        // 列挙したい項目
        String[] items_for_lv = {
            "A",
            "B",
            "C"
        };

        // 組み込み済みの行レイアウトを使ってアダプタを生成
        ArrayAdapter<String> adapter_for_lv = new ArrayAdapter<String>(
            this,
            android.R.layout.simple_list_item_1,
            items_for_lv
        );

        // リストにアダプタをセット
        lv = (ListView)findViewById(R.id.listview1);
        lv.setAdapter(adapter_for_lv);

        // ボタンにイベント定義
        btn = (Button)findViewById(R.id.btn1);
        btn.setOnClickListener(this);

        // カウントを表示
        tv = (TextView)findViewById(R.id.textView1);
        showListViewCount();
            // 出力:
            // getChildCount = 0,
            // getCount = 3,
            // tr_view is null

    }

    // リストビューに関する情報を表示
    private void showListViewCount() {
        // リストの要素数をカウント
        int child_cnt = lv.getChildCount();
        int raw_cnt = lv.getCount();

        // カウントを表示
        tv.setText(
            "getChildCount = "
            + child_cnt
            + ", getCount = "
            + raw_cnt
        );

        // null判定
        View tr_view = lv.getChildAt(0);
        if( tr_view == null )
        {
            tv.setText( tv.getText() + ", tr_view is null" );
        }
        else
        {
            tv.setText(
                tv.getText()
                + ", tr_view is not null : "
                + tr_view.getClass().getName()
            );
        }

    }

    @Override
    public void onClick(View v) {
        if( v == btn )
        {
            showListViewCount();
                // 出力:
                // getChildCount = 3,
                // getCount = 3,
                // tr_view is not null : android.widget.TextView
        }
    }
}


main.xmlによるレイアウト:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >

    <!-- テキスト表示 -->
    <TextView
        android:id="@+id/textView1"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="@string/hello"
    ></TextView>

    <!-- リスト -->
    <ListView
        android:id="@+id/listview1"
        android:layout_width="fill_parent"
        android:layout_height="200dp"
        android:layout_marginBottom="30dp"
    />

    <!-- ボタン -->
    <Button
        android:id="@+id/btn1"
        android:layout_height="wrap_content"
        android:layout_width="300dp"
        android:text="押す"
    ></Button>

</LinearLayout>

実行結果は,コメントに書いてある通り。


検証結果:要素数

まずは,リストビュー中の要素数から。


getCount() は,常に「3」を返している。

これは,getCount() がビューではなくアダプタの情報を参照するためだ。

アダプタにセットした要素の数は,問答無用で3なので,いつも3が返る。

マニュアル:
The number of items owned by the Adapter associated with this AdapterView. (This is the number of data items, which may be larger than the number of visible views.)


getChildCount() は,別物。「見えている行」の数を返す。

画面の描画処理が完了しているか・いないかに応じて,

またListView中で見えている行の数に応じて,返却値が変動する。


ウソだと思ったら,items_for_lvの要素数を増やして実行してみよう。

行が30行の場合,getCount() は30を返すが,

getChildCount() はボタン押下時に3とか4のような小さな値を返す。



そして,onCreate() 内では getChildCount() で0が返ってくる,というのは曲者。

lv.invalidate(); のように無理やり再描画しても効果なし。

アダプタのセット直後は,getChildCount() で正常な値は返ってこないのである。



getCount() と getChildCount() の違い:

In an android ListView, how can I iterate/manipulte all the child views, not just the visible ones?
http://stackoverflow.com/questions/21...

  • "getChildCount()" does not get all of a ListView's rows, but just the rows that are visible.
  • In a ListView the only children are the visible ones. If you want to do something with "all the children," do it in the adapter. That's the best place.


adapter getCount and listView getChildCount are not equal
http://stackoverflow.com/questions/55...

  • getChildCount() refers to the ViewGroup's method that returns the number of the views that this view contains, it's not ListView's method itself.


Android ListView - 開発Tips
http://d.hatena.ne.jp/nasu_t/20110828...
フッターのVisibilityや表示内容を制御する場合は、ListView#getCountに気をつけましょう。
ヘッダー、フッターの分も数えて制御しないといけません。
※Item10個をAdapter#addしてもListView#addFooterViewを実行しているとListView#getCountは11を返します。

検証結果:アイテムの取得

次に,リストビュー中のアイテムの取得結果について。

onCreate() の最中には null で,ボタン押下時にはちゃんとView要素が返る。

つまり,画面の描画が終わっているか・いないかに左右されてしまう。

getChildAt() returns null if its not in the visible area
http://groups.google.com/group/androi...
ListView contains only the number of views
needed to fill the screen. Therefore you cannot access the views
outside of the screen since they do not exist.

Override getView() in your adapter and do whatever you want to the row
Views as they are created.


ListView#getChildAtとListView#getItemAtPositionがnullになる。
http://d.hatena.ne.jp/nasu_t/20110828...

  • 本来あるはずのpositionを指定して、ListView#getChildAtやListView#getItemAtPositionを呼び出しても、高速にスクロールを繰り返していると、nullが返却されることがあります。


ViewGroup直下にある全ての子Viewにアクセスするには
http://blog.global-eng.co.jp/android/...

  • View getChildAt(int index)⇒自身に設定されている子Viewの場所を指定して取得する
  • int getChildCount()⇒自身に設定されている子Viewの数を取得する
  • レイアウトに設定されている直下のViewに対して全てアクセスできます

対策

月並みな回答になるのだが・・・

  • (1)「しばらく時間をおいてからアクセスし直してください」。(対象UIの描画完了後に操作する)
  • (2)Adapterクラスを素で使わず,拡張して独自実装する

といったところか。

AndroidアプリでListViewをカスタマイズし,Web上の画像を行ごとに表示するサンプルコード (SimpleAdapterクラスを独自に拡張)
http://language-and-engineering.hatenablog.jp/entry/20111014/p1

補足

下記は,全然別のエラーに関して。


TextViewのsetText() の引数にint型の変数を渡してしまうと,

stringリソースのIDと勘違いされてしまい,

「NotFoundException: String resource ID #0x3」のようになる。

(本当は「3」と表示したいだけなのに。)

回避するためには,"" + num のようにStringにしてあげる必要がある。

ERROR/AndroidRuntime(2724): Uncaught handler: thread main exiting due to uncaught exception
ERROR/AndroidRuntime(2724): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example/com.example.activity.hoge.CodeTestActivity}: android.content.res.Resources$NotFoundException: String resource ID #0x3
ERROR/AndroidRuntime(2724): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2503)
ERROR/AndroidRuntime(2724): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2519)
ERROR/AndroidRuntime(2724): at android.app.ActivityThread.access$2200(ActivityThread.java:123)
ERROR/AndroidRuntime(2724): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1870)
ERROR/AndroidRuntime(2724): at android.os.Handler.dispatchMessage(Handler.java:99)
ERROR/AndroidRuntime(2724): at android.os.Looper.loop(Looper.java:123)
ERROR/AndroidRuntime(2724): at android.app.ActivityThread.main(ActivityThread.java:4370)
ERROR/AndroidRuntime(2724): at java.lang.reflect.Method.invokeNative(Native Method)
ERROR/AndroidRuntime(2724): at java.lang.reflect.Method.invoke(Method.java:521)
ERROR/AndroidRuntime(2724): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
ERROR/AndroidRuntime(2724): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
ERROR/AndroidRuntime(2724): at dalvik.system.NativeStart.main(Native Method)
ERROR/AndroidRuntime(2724): Caused by: android.content.res.Resources$NotFoundException: String resource ID #0x3
ERROR/AndroidRuntime(2724): at android.content.res.Resources.getText(Resources.java:200)
ERROR/AndroidRuntime(2724): at android.widget.TextView.setText(TextView.java:2843)
ERROR/AndroidRuntime(2724): at com.example.activity.hoge.CodeTestActivity.onCreate(CodeTestActivity.java:45)
ERROR/AndroidRuntime(2724): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1047)
ERROR/AndroidRuntime(2724): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2466)
ERROR/AndroidRuntime(2724): ... 11 more
ERROR/SemcCheckin(2724): Get crash dump level : java.io.FileNotFoundException: /data/semc-checkin/crashdump

補足2:他のプラットフォームでの同様のエラー

上記で取り上げたのはAndroidアプリのUIで発生する問題だが,

他のUI実装技術でも,同様の問題が起こりうる。


例えば,Silverlight。

WPFでGUIを構築する際,リストビュー的な部品を配置したら,画面外の部分はオブジェクトとして開放された状態になり,操作できない。

MVVMパターンとは? わんくま同盟東京勉強会 #60 セッション資料
http://ugaya40.net/mvvm/what_is_mvvm....

  • スライドの24ページ:コレクションコントロールの罠
    • 表示されていない範囲のインスタンスは生成されていないため,見えていない範囲のオブジェクトを触ろうとしても触れない