2012/02/17

setOnClickListener の実装方法

Android プログラムを書いていると、イベントリスナを実装することは日常茶飯事ですが、「インターフェースを実装した実体であれば何でもよい」という緩い制約のため、いろいろな書き方があります。

普段何気なく使い分けているのですが、どのように使い分けているのか自分の整理も含めて書いてみることにしました。
とりあえず、思いついた4つを書いておきますが、この辺の分け方は恣意的です


  1. 既存のクラスにインターフェースを実装する
    public class MyView extends View
            implements android.view.View.OnClickListener {
        // 他のコンストラクタは省略
        public MyView(Context context) {
            super(context);
    
            setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            // TODO Auto-generated method stub
        }
    }

    このように、自分自身にインターフェースを実装して、onXXXListener に this を与えてやる方法です。
    この方法、よく見かけるのですが、私は多用しないようにしています。
    例えば、以下のように Activity に実装した場合のことを考えます。

    public class MyActivity extends Activity implements OnClickListener {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            View titleView = findViewById(R.id.title);
            titleView.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View v) {
            // TODO Auto-generated method stub
        }
    }

    この場合、Viewの数が増えてきた場合に対応できませんし、それを置いておいたとしても、「titleView をクリックしたら、Activity の onClick が呼ばれる」という状況は分かりにくいこと極まりません。
    私は、「名は体を表す」ように実装してあげるように心がけるべきだと思います。

  2. 匿名クラスを使う 1
    public class MyActivity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            View titleView = findViewById(R.id.title);
            titleView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    // TODO Auto-generated method stub
                }
            });
        }
    }

    この方法もよく見かけますし、私もよく使います。
    慣れるまではちょっと見にくいかもしれませんが、慣れてしまえば、簡単な処理なら内容をすぐ確認できるため便利です。
    ただ、複雑な処理を書く場合は見にくくなったりする難点があります。
    また、クリックされた時の処理が状態によって変わったりする場合、setOnClickListener を呼ぶたびにインスタンスが生成されるため、多少パフォーマンス的に難ありだと思われます。

  3. 匿名クラスを使う 2
    public class MyActivity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            View titleView = findViewById(R.id.title);
            titleView.setOnClickListener(showAlertDialog);
        }
    
        private final OnClickListener showAlertDialog = new OnClickListener() {
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                
            }
        };
    }

    「匿名クラスを使う 1」の問題を回避するために私が時々使う方法です。
    メンバ変数に OnClickListener への参照を持たせておくわけです。
    クリックしたときの処理が複雑で長くなる場合や、別々のボタンに同じ処理を持たせたい場合に便利です。
    ただし、メンバ変数を持たせると、いろいろなボタンで使いまわせなくなるので要注意です。
    この記述、あまり見かけないのは、そういう意味でJavaの思想からずれているからかもしれません
    (まー、1番目の方法「既存のクラスにインターフェースを実装する」でも、複数のボタンに対して使いまわすと、同じ問題が起こりますけどね…)
    2012/2/22 追記 「あまり見かけない」というのは勘違いでした。Android のソースコードの中でも結構使っていますね。

    この方法で一番のメリットは、インスタンスの生成が1度きり。というところです。
    Android のようなリソースが限られた環境の場合は、意味がある場合もあります。しかし、メリットとデメリットをよく理解した上で使用しないと痛い目にあうかも…。

    また、この方法、クラスは匿名なのですが、メンバ変数に名前が必要で、その命名が難しいことが難点です。
    私は、setOnClickListener 近辺を読んでいるときに処理の流れがわかるよう、showAlertDialog のように、動作を表す変数名にするようにしているのですが、この方法だと Listener であることが分かりにくくなってしまいます。
    だけど、showAlertDialogListener では意味が変わってしまいます。
    本来なら、 onClickListenerForShowAlertDialog とでも付けるのが良いのでしょうが、「長い割にわかりにくいかなー」と思い採用していません。

  4. リスナクラスを定義する
    public class MyActivity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            View titleView = findViewById(R.id.title);
            titleView.setOnClickListener(new ShowAlertDialog());
        }
    
        private class ShowAlertDialog implements OnClickListener {
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                
            }
        };
    }

    ある意味、一番王道です。
    OnClickListener の実装をきちんと定義してあげるわけですね。
    この問題も、3番同様、名前の付け方が難しいという問題を持っています。

    上記では private なインナークラスとして実装していますが、インナークラスとして実装出来るような場合は、3番の方が使い易いでしょう。(インスタンスの生成タイミングを制御したい場合などは別ですが)
    この方法は様々なクラスで同じイベントリスナを使いたいというような場合に使えます。(そういうケースはあまり多くないと思いますが…)


これらの方法、Activity と View のようにライフサイクルを共にしている場合は問題ありませんが、センサー等にリスナを登録する場合、Activity が見えなくなっても、センサからの参照は生きているということに気を付けてください。
放っておくとOutOfMemoryで落ちてしまいます。
その辺についてはまた別の機会に書こうと思います。

2 件のコメント:

初心者 さんのコメント...

なるほど、最近勉強始めたのですが1と2しかみたことありませんでした。
よくまとまってて分かりやすかったです(^^)

Yusuke Miura さんのコメント...

お役に立てたようでよかったです!