【Android】ListViewの各データにイベントを設定する

ListView、もしくはそれに設定されたAdapterから要素を取得するには、どうあがいてもpositionを使って取得することになります。

そして、ListViewにonItemClickListenerを設定しつつ、クリックされた場所に応じて動的に処理を変更させたい場合は、positionそのものか、positionから取得した要素を使わざるを得ません。

それはまぁいいんですが、例えばこんな場合。

public class TestActivity extends Activity {

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

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        adapter.add("hoge");
        adapter.add("fuga");
        adapter.add("piyo");
        
        ListView listView = (ListView) findViewById(id.listView);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view,int position, long id) {
                ListView listView = (ListView) parent;
                String item = (String) listView.getItemAtPosition(position);
                
                if(item.equals("hoge") {
                    //TODO:hoge
                } else if(item.equals("fuga") {
                    //TODO:fuga
                } else if(item.equals("piyo") {
                    //TODO:piyo
                }
                
            }
        });
    }
}

これがですね、ほんと嫌なんですよ。ちょっと文字列だけ表示してクリックされたらイベントを実行するだけなのに、なんで表示されてる文字列から判定しなきゃいけないのかと。

文字列なせいでコード補完は効かないし、文字列なせいでちょっと表示する値を変えたいときも面倒だし。

いや確かに、xmlのリソース使えって話かもしれないんですが、私はあれも嫌いです。どこで使われてるかよくわからなくなるし、直に書いてあったほうが絶対にソースは読みやすいです。正直、翻訳に関する問題を気にしないなら、あれを使う理由がないと思ってます。

適当にprivate static final intなメンバーを作っておいて、positionでswitchする方法もあります。今回のパターンならそれでもまぁギリギリ許容範囲ですが、これがもし仮に動的に追加/変更されていくパターンだったら、一瞬にして瓦解します。

結局、取得したitem自体がイベントを持っていたらそれで済むんですよ。item#invoke()みたいなメソッドを呼び出すと事前に設定しておいた動作を行う。そんなことが出来ればそもそも判定なんかしなくていいはずです。

クロージャクロージャを(ry

そんなわけでクロージャの出番です。まずはこんなクラスを作ります。

public class ClickableItem<EventArg> implements Serialize {

    private static final long serialVersionUID = -1L;
    private String name;
    private V1<EventArg> event;
    
    public ClickableItem(String name, V1<EventArg> event) {
        this.name = name;
        this.event = event;
    }
    
    //各アクセサは面倒なので割愛
    
    public void invoke(EventArg arg) {
        this.event.call(arg);
    }
    
    @Override
    public String toString() {
        return this.name;
    }
    
}

さっきの例はこんな感じにします。

public class TestActivity extends Activity {

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

        ArrayAdapter<ClickableItem<String>> adapter = new ArrayAdapter<ClickableItem<String>>(getApplicationContext()
                        , android.R.layout.simple_list_item_1);

        adapter.add(new ClickableItem<String>("hoge", new V1<String>() {
            @Override
            public void call(String arg0) {
                //TODO:hoge
            }
        });
        
        adapter.add("fuga", new V1<String>() {
            @Override
            public void call(String arg0) {
                //TODO:fuga
            }
        });
        
        adapter.add("piyo", new V1<String>() {
            @Override
            public void call(String arg0) {
                //TODO:piyo
            }
        });
        
        ListView listView = (ListView) findViewById(id.listView);
        listView.setAdapter(adapter);
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view,int position, long id) {
                ListView listView = (ListView) parent;
                ClickableItem<String> item = ClickableItem<String> listView.getItemAtPosition(position);
                item.invoke(item.getName());
            }
        });
    }
}

豆知識ですが、独自クラスであってもtoStringをちゃんとオーバーライドしておけば、android.R.layout.simple_list_item_1のようなリソースレイアウトでもちゃんと表示されます。ArrayAdapterならequalsとhashCodeメソッドをオーバーライドすることでitemからpositionを割り出したり、removeメソッドを使ったり出来ます。実際に作るときはサボらずちゃんと実装しておきましょう。

まとめ

どんなクラスでもV1<EventArg> eventをメンバとして持たせておけばこのパターンはいつでも適用できます。また、ListViewでなくとも、AdapterViewを継承しているクラスなら(Spinnerとかでも)当然可能です。

サンプルが単純すぎるのでメリットがわかりにくいかもしれませんが、例えばListViewを保持するDialogFragmentをメニューダイアログとして使いたい、なんて時には非常に便利です。setArgumentでClickableItemのArrayListを渡すことで、表示する内容とイベントはすべて呼び出し元で記述する超汎用的なオレオレメニューダイアログを作ることが出来ます。

呼び出し元のActivity/FragmentでAdapterView.OnItemClickListenerをimplementsすることで同じことが出来ますが、こっちの方が圧倒的にスマートだと思います。