OnClickListenerの実装方法について煩悶としています。
どのように書くのが美しいのでしょうか。
Activity,Fragmentに実装する方法
Button等を表示するActivityやFragmentに実装させる方法があります。
public class SampleFragment extends Fragment implements OnClickListener {
// ...
@Override
public void onClick(View v) {
// 押下時の処理
}
}
一般的だと思います。
よく見るコードですが、個人的にはイマイチだと思っています。
イマイチな点1: publicなonClick
個人的にpublicなonClick()
というメソッドが作られてしまうことが気になります。
下記のようなことができてしまいます。
SampleFragment fragment = new SampleFragment();
fragment.onClick(null);
やりませんけどね。
また、以下のようなこともできてしまいます。
SampleFragment fragment = new SampleFragment();
View button = findViewById(R.id.button1);
button.setOnClickListener(fragment);
やりませんよ?
やりませんが、できてしまうということが気になります。
イマイチな点2: 1つしかないonClick
この方法は、クリック時の処理を一箇所にまとめられるメリットがあるようです。可読性が高まるのでしょうか。
switch
でViewのID毎に処理を振り分けるのが一般的だと思います。
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
// ○○○
// □□□
break;
case R.id.button2:
// □□□
// ○○○
// ...
break;
case R.id.button3:
// □□□
// ...
// ○○○
break;
}
}
美しくないですね。
可読性を意識される方は、こんないろんな処理がダラダラと続くようなコードを書いたりはしないはず。
ボタン毎にメソッドに切り出しましょう。
private void onClickButton1() {
// ○○○
// □□□
}
private void onClickButton2() {
// □□□
// ○○○
// ...
}
private void onClickButton3() {
// □□□
// ...
// ○○○
}
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
onClickButton1();
break;
case R.id.button2:
onClickButton2();
break;
case R.id.button3:
onClickButton3();
break;
}
}
メソッドが小さくなって読みやすくなりました。
しかし、こうなるとonClick()
が大した仕事をしていません。
こんなonClick()
が必要でしょうか。
無名クラスとしてprivateフィールドに持つ方法
大した仕事をしないonClick()
を使わないように、ボタン毎にOnClickListener
を用意するようにしましょう。
OnClickListener button1ClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
// ○○○
// □□□
}
};
OnClickListener button2ClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
// □□□
// ○○○
// ...
}
};
OnClickListener button3ClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
// □□□
// ...
// ○○○
}
};
クリック時の処理が必要なViewに対して、上記OnClickListener
のインスタンスを渡してあげれば良いですね。
view.findViewById(R.id.button1).setOnClickListener(button1ClickListener);
view.findViewById(R.id.button2).setOnClickListener(button2ClickListener);
view.findViewById(R.id.button3).setOnClickListener(button3ClickListener);
onClick()
を消したので、FragmentにimplementsさせていたOnClickListener
も外しておきます。
public class SampleFragment extends Fragment {
// ...
}
publicなonClick()
がなくなりました。
Activity,FragmentにOnClickListenerを実装するよりは、正しい姿になったように思います。
直接setOnClickListener()に無名クラスを渡す方法
privateフィールドにインスタンスを保持させても良いのですが、他で使うわけでもなし、無駄な気がします。
直接setOnClickListener()
に渡してしまいましょう。
view.findViewById(R.id.button1).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// ○○○
// □□□
}
});
// ... 以下略
これもサンプル等でよく見る形です。
無駄は削ぎ落とされてきましたが、ちょっと読みにくい気がします。
setOnClickListener()
の中の記述量が多いからでしょうか。
以下のように一時変数に代入しておくと多少は読みやすくなるような気がします。
OnClickListener button1ClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
// ○○○
// □□□
}
};
view.findViewById(R.id.button1).setOnClickListener(button1ClickListener);
// ... 以下略
いかがでしょうか。
専用のクラスに実装する方法
一時変数に代入したとしても、setOnClickListener()
を行うメソッドにクリック時の処理という異物が紛れ込んでいることには変わりありません。
読みやすさという点では低下しているように感じます。
いっそクリック時の処理をクラスにしてしまいましょうか。
class Button1ClickListener implements OnClickListener {
@Override
public void onClick(View v) {
// ○○○
// □□□
}
}
OnClickListener
を設定するところは以下のようにすっきりします。
view.findViewById(R.id.button1).setOnClickListener(new Button1ClickListener());
すっきりはしますが…、ここまでやると大仰な気がしますね。
ここまでしなくても、処理を別の場所に書きたいだけであれば、「無名クラスとしてprivateフィールドに実装する方法」で十分に思います。
結局
OnClickListenerの実装方法として、4つを挙げました。
- Activity,Fragmentに実装する方法
- 無名クラスとしてprivateフィールドに持つ方法
- 直接setOnClickListener()に無名クラスを渡す方法
- 専用のクラスに実装する方法
どの方法も一長一短あり、どれが一番良いとは決められません。
作成するアプリにもよるかもしれません。
私は2番目の「無名クラスとしてprivateフィールドに持つ方法」で実装することが多いです。
クラスとしての形、読みやすさのバランスが丁度良い気がしているからです。
みなさんはどのように実装しているでしょうか。
どのように実装されているか、理由付きで教えていただけると幸いです。
追記 (2014/03/02)
amay077さんからコメントをいただきました。
上記3以外の方法では
他所から参照される可能性を生みます。
だそうです。確かにそのとおりでした。
正しくないコードが書けてしまう
1で関係ないViewのsetOnClickListener()
が呼べてしまったように、2,4でも同様のことができてしまいます。
// 2でも↓こんなことが可能
view.findViewById(R.id.text5).setOnClickListener(button1ClickListener);
// 4でも↓こんなことが可能
view.findViewById(R.id.check4).setOnClickListener(new Button1ClickListener());
これは美しくないですね。
1と同様に、自分ひとりで開発していたのならば、こんなコードは書かないとは思いますが、間違えてしまう可能性はゼロではありません。
指摘していただいた点を踏まえると、「美しさ」は「3. 直接setOnClickListener()に無名クラスを渡す方法」が一番良いように思えます。
Androidのコードはどうなってる?
Androidのソースコードではどのように実装されているのか以下のサイトで検索してみました。
Androidソースコード検索サービス - Developer Collaboration Project
キーワードは「setOnClickListener\(」で検索しました。
検索結果をすべて確認したわけではないのですが、ざっと何ページか見てみただけでも「3. 直接setOnClickListener()に無名クラスを渡す方法」が圧倒的に多いことがわかります。
ときどき、「2. 無名クラスとしてprivateフィールドに持つ方法」が検索結果に現れ、ごく稀に「1. Activity,Fragmentに実装する方法」も使われているようです。
「4. 専用のクラスに実装する方法」は見つけられませんでした。
結局どうなの
「3. 直接setOnClickListener()に無名クラスを渡す方法」が美しい理由を知ることができました。Androidのコードでも多数この方法で実装されているので、「3の方法が一番美しい」と決めつけてしまっても良いかも知れません(ただ、「それ、ループの中でも同じこと言えんの?」というツッコミはありそうです)。
他にもご意見ございましたら、是非お聞かせください。
よろしくお願いします。
追記 (2014/03/03)
zaki50さんからもコメントをいただきました。
私は最近だと Android Annotations か Butter Knife をつかったプロジェクトばかりなので、
Activity とかのクラスに @OnClick とか @Click なアノテーションつきのメソッドを用意するだけです。
私はどちらもちゃんと使ったことがなかったのですが、アノテーションをつけるだけでクリックイベントを処理できるというのは聞いたことがありました。
せっかくの機会なので試してみました。
AndroidAnnotations
上記リンク先でもサンプルコードを見ることができますが、OnClickListenerに関する部分は以下のように書くことができます。
@EActivity(R.layout.activity_main)
public class MainActivity extends Activity {
@Click
void buton1() {
// ○○○
// □□□
}
@Click
void button2(View v) {
// □□□
// ○○○
// ...
}
@Click(R.id.button3)
void hoge() {
// □□□
// ...
// ○○○
}
}
すごいですね。
上記のコードだけで、setOnClickListener()
相当の処理も含まれています。
ちょっと調べただけでも、以下のことができるようです。
-
@Clickをつけたメソッドは、メソッド名と同じIDを持つViewのClickListenerとして扱われる(
R.id.button1
←→button1()
) -
OnClickListener
と同じように引数にViewを取ることができる - @ClickにViewのIDを指定すると、任意のメソッド名をつけることもできる
- メソッドをprivateにすることはできないっぽい
他にもできることがあるかもしれませんが、パッとわかったのはこれくらい。
Butter Knife
同様にButter Knifeを使った場合、以下のように書くことができます。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.inject(this);
}
@OnClick(R.id.button1)
void clickButton1() {
// ○○○
// □□□
}
@OnClick(R.id.button2)
void clickButton2(Button button) {
// □□□
// ○○○
// ...
}
@OnClick({ R.id.button3, R.id.button4 })
void clickOtherButton(Button button) {
// □□□
// ...
// ○○○
}
}
こちらもすごいですね。
@OnClickにViewのIDを指定することは必須のようですが、AndroidAnnotationsとは違った便利さがありそうです。
- @OnClickにViewのIDを指定して利用する。メソッド名は任意の名前
- 引数を取ることができる上、キャストした状態で受け取ることができる
- 複数のViewのクリックイベントをまとめて受けることができる
- こちらもメソッドをprivateにはできないっぽい
試してみた感想
確かにこれらを使うとコードがすっきりします。
これらのライブラリは、メソッド名を自由に付けられたり、引数の有無を選択できたりと、美しいコードを書けるように作られているんだな、とも感じました。私もこれからのプロジェクトでは積極的に使っていきたいと思いました。
こういったライブラリはどれくらいの割合の人たちが利用しているんでしょう?
また、先に挙げた4つの実装方法の選ばれている割合も気になります。
Qiitaにアンケートのような機能があったらよかったのに。
追記 (2014/03/09)
shunsugai@githubさんからコメントいただきました。
layoutのxmlでandroid:onClick属性を使うという方法もあります。
確かにそんなやり方もありました。
shunsugaiさんも「あまり使いませんが…」と前置き付きで紹介くださいました。
せっかくなので使い方を書いておきます。
layout.xmlに以下のようにandroid:onClick
という項目を記述します。
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="clickButton1"
android:text="Button" />
その上で、Activityに以下のように書きます。
public void clickButton1(View view) {
// ...
}
これだけでこのメソッドが呼ばれるようになります。
ActivityにOnClickListenerを実装する必要もありませんし、setOnClickListener()を呼ぶ必要もありません。
これだけ見ると便利そうな感じもしますが、これ、美しくないんですよね…。
- ActivityにOnClickListenerを実装する方法と同様に、publicなメソッドを実装しなければいけない。
- Fragmentにはこの方法が使えない。Activityにしか使えない(たぶん?)。
- メソッドを実装し忘れる(もしくは名前を間違える等)すると、実行時に例外が発生する。
- Activityのみを見たときに、メソッドがどこから呼ばれているのかわからない。
などなど。
今、使っている人はあまりいないんではないでしょうか。
せっかくコメントを頂いたので追記してみました。
初めからこれくらい網羅して「OnClickListener実装方法まとめ」というようなタイトルで公開した方が良かったかもしれませんね。