ButterKnifeが素敵だ

  • 192
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

すでにいくつものブログで紹介されていたり、書籍でも取りあげられたライブラリ。

Androidの View Injection ライブラリ、ButterKnife。

基本情報、導入方法はすでに詳しく取り上げている方々がいらっしゃいますので、今回は書いていません。

公式サイトは読みやすいですし、
@yyaammaa さんのButter Knifeの紹介(Qiita)

yyaammaa(githubアカウント) さんのButter Knifeの紹介(gist)
そして
hotchemiさんのバナーナイフの用法とその効能(時速5km)
などのページ・投稿でとても分かりやすく紹介されています。

【追記ここから】

「ここが残念の」節でOnItemClickとOnItemLongClickにversion 4.0.1バグがあると書いていましたが、Eclipseの場合だけで、AndroidStudioの場合は、version 4.0.1でも正常に利用できました。

また、このバグはversion 5.0.0で修正されたようです。

【追記ここまで】

基本的な使い方

一応、基本的な使い方を示します。
activity_main.xmlには、idがtextViewなTextViewとidがbuttonなButtonが定義されています。

次のコードは、onCreate内で、mTextViewに"Hello ButterKnife!"という文字列をセットし、idがbuttonのButtonをタップしたら、Toastを表示するようリスナーをセットするコードです。

ButterKnife導入前
/*

*/
public class MainActivity extends Activity {

    TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = (TextView) findViewById(R.id.textView);
        mTextView.setText("Hello ButterKnife!");

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                showToast();
            }
        });
    }

    void showToast() {
        Toast.makeText(this, "Clicked!", Toast.LENGTH_LONG).show();
    }
}

これをButterKnifeを使って書き換えます。ポイントは、

  • mTextView変数に@InjectView(R.id.textView)を付与
  • showToastメソッドに@OnClick(R.id.button)を付与
  • setContentViewの呼び出し後、mTextViewの利用前に、ButterKnife.inject(this)を呼び出し
  • findViewByIdが無くなっている
  • setOnClickListerが無くなっている

です。

ButterKnife導入後
/*

*/
public class MainActivity extends Activity {

    @InjectView(R.id.textView)
    TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.inject(this);

        mTextView.setText("Hello ButterKnife!");
    }

    @OnClick(R.id.button)
    void showToast() {
        Toast.makeText(this, "Clicked!", Toast.LENGTH_LONG).show();
    }
}

@InjectViewアノテーションを利用すれば、findViewByIdを省略出来ます。
@OnClickを使えば、冗長なsetOnClickListerやOnClickInterfaceを実装した匿名クラスの定義をせず、指定idをクリックした際の処理を定義できます。

ButterKnife.inject(this)を呼び出すのをお忘れなく。

素敵だと思ったところ

個人的に素敵だと思ったことをつらつら書いていきます。

ButterKnife#findViewをstaticインポートして使えばスッキリ

fineViewByIdメソッドはやはり必要

InjectViewアノテーションをフィールドに指定すれば、findViewByIdでidを指定しViewを取得でき、ButtonやImageViewなどの実際の型にキャストする必要もありません。

ですが、これでfindViewByIdが必要ないかといえばそうではありません。次のようにfindViewByIdの結果をローカル変数に代入し、そのメソッドのスコープで処理を完結させたい場合です。

findViewById
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    TextView textView = (TextView) findViewById(R.id.textView);
    textView.setText("Hello ButterKnife!");
}

フィールドとして扱えばInjectViewアノテーションを使えますが、フィールドはそのインスタンス全体から参照できます。インスタンススコープでは広すぎる、ローカルスコープで完結させたいという場合もあります。やはり、findViewByIdメソッドを使う必要がありますね。

面倒なキャストの記述を、ButterKnife#findByIdメソッドで

findViewByIdメソッドはint型でViewのidを引数に取り、Viewを返します。Button、ImageViewなどの型として扱いたい場合は、キャストする必要があります。

キャストの記述、面倒くさいですね。

ButterKnifeには、この箇所をスッキリさせてくれるメソッドがあります。ButterKnife#findByIdというメソッドです。

ButterKnife#findByIdを使う
TextView textView = ButterKnife.findById(this, R.id.textView);

これを用いればキャストする必要はありません。
代入する変数の型に型推論してくれて、メソッド内部でキャストしてくれます。
ただ文字数的には、findViewByIdを使う場合と大して変わりませんね。

そこで、ButterKnife#findByIdメソッドをstaticインポートします。

ButterKnife.findByIdメソッドをstaticインポート
import static butterknife.ButterKnife.findById;

そうすれば、次のようにスッキリと書くことができますね。

ButterKnife#findByIdを使う
TextView textView = findById(this, R.id.textView);

Fragmentでも便利、ButterKnife#findById

このButterKnife#findById、Activity内で使うのもスッキリしますが、Fragment内で使うのはもっとスッキリします。

例えば、Fragment内のonCreateViewメソッド内でfindViewByIdをする場合、

TextView textView = (TextView) view.findViewById (R.id.textView);

このように記述する必要があります。onCreateViewメソッド内でインフレートしたviewで、findViewByIdメソッドを呼び出す必要があります。

長いですね。

ここで、ButterKnife#findByIdメソッドの、Viewとint型を引数にとる方のオーバーロードを用いるとスッキリします。

TextView textView = findById(view, R.id.textView);

スッキリしましたね。

このように、findByIdメソッドとstaticインポートを使えば、キャスト処理を記述する必要が無くなります。
InjectViewアノテーションやOnClickアノテーションのように劇的には変わりませんが、Viewのキャストというちょっと煩わしくて、冗長だった記述がなくなり、スッキリします。

OnClickアノテーションいろいろ

ButtonなどViewのサブクラスを引数にとるメソッドを指定でき,キャストの手間が省ける。

ButtonがクリックされたらButtonに何らかの変更をし、加えて何らかの処理を実行する場合を考えます。

findViewById(R.id.button).setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Button button = (Button) v;
        button.setText("clicked.");
        // 何らかの処理
    }
});

ButterKnifeを使わない場合、onCreateメソッド内で次のように記述するかと思います。
これを素直にButterKnifeで書き換えてみます。

@OnClick(R.id.button)
void onClickButton(View v) {
    Button button = (Button) v;
    button.setText("clicked!");
    // 何らかの処理
}

上記のようなOnClickアノテーションを付与したメソッドをActivity内に定義します。このようにも書けるのですが、次のように書きえると更にスッキリします。

@OnClick(R.id.button)
void onClickButton(Button button) {
    button.setText("clicked.");
    // 何らかの処理
}

このように書けば、メソッド内でキャストする手間が省けますね。

ただし、次のような場合には注意が必要です。buttonというidが示すViewがButtonではなくて、ImageViewだった場合、次のコードはImageViewがクリックされた時点で実行時例外が発生してしまいます。

@OnClickアノテーションで実行時例外
@OnClick(R.id.button)
void onClick(Button button) {
// 以下略

Viewではなくそのサブクラスの型を指定する場合、そのidのViewのインスタンスがその型のインスタンスであるか、その型のインスタンスにキャスト可能であるかをしっかりと確認する必要があります。

ちなみに、次のような引数なしメソッドも指定できます。

@OnClick(R.id.button)
void showToast() {
    Toast.makeText(this, "Hello ButterKnife!", Toast.LENGTH_LONG).show();
}

複数のidを指定できる

OnClickアノテーションに、複数のidを指定することができます。

電卓のようなアプリを考えます。
0から9までのボタンがあって、タップされたときの処理をButterKnifeを使って定義する場合、次のように記述することができます。

@OnClick(R.id.button0)
void onClickButton0() {
    onClickNumberButton(0);
}

@OnClick(R.id.button1)
void onClickButton1() {
    onClickNumberButton(1);
}

// 略

@OnClick(R.id.button9)
void onClickButton9() {
    onClickNumberButton(9);
}

void onClickNumberButton(int number) {
    // numberを使った処理
}

上記のように書いてもいいのですが、同じようなメソッドが10個できてしまいますね。

OnClickアノテーションは複数のidを指定できるので、次のように書くことができます。

@OnClick({
    R.id.button0,
    R.id.button1, R.id.button2, R.id.button3,
    R.id.button4, R.id.button5, R.id.button6,
    R.id.button7, R.id.button8, R.id.button9,
})
void onClickNumberButton(Button numberButton) {
    int number;
    switch (numberButton.getId()) {
    case R.id.button0:
        number = 0;
        break;
    case R.id.button1:
        number = 1;
        break;
    // 以下略
    case R.id.button9:
        number = 9;
        break;
    default:
        throw new IllegalArgumentException();
    }
    // numberを使った処理
}

switch文が必要ですが、メソッド一つで処理できます。

指定したメソッドが、明らかに正しくない場合コンパイルエラーになってくれる

次のようなメソッドに@OnClickアノテーションを付与した場合、しっかりとコンパイルエラーになってくれます。

  • Viewを継承していない型を引数にとるメソッド
  • 2個以上の引数をとるメソッド

実行時ではなくて、コンパイルエラーになるのは助かりますね。

複数のメソッドに同じidを指定した場合コンパイルエラーになってくれる

次のコードを見てください。何か変なことに気づきませんか?

@OnClick(R.id.button)
void onClick0(Button button) {
    Toast.makeText(this, "onClick0", Toast.LENGTH_LONG).show();
}

@OnClick(R.id.button)
void onClick1() {
    Toast.makeText(this, "onClick1", Toast.LENGTH_LONG).show();
}

R.id.buttonに対してのOnClickアノテーションの指定を2回行っていますね。論理的におかしいですよね。

これは、しっかりコンパイルエラーになってくれます。
エラーメッセージで、多重にOnClickアノテーションが指定されていることも示してくれます。

@OnCheckedChangedで指定するメソッドも引数を柔軟に扱える

CheckBoxやToggleButton、Switchなどのスーパークラス、CompoundButton。
このクラスはチェック、未チェックの二つの状態を持ち、クリックされると、今の状態とは異なる状態に変化するクラスですね。

状態が変わったことをハンドリングするために、OnCheckedChangeListenerを用いますよね。

ButterKnifeなしで、setOnCheckedChangeListener
CompoundButton compoundButton = (CompoundButton) findViewById(R.id.toggleButton);
compoundButton.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        // 何らかの処理
    }
});

OnClickアノテーションのように、この記述を簡略化できるアノテーションがあります。
OnCheckedChangedアノテーションです。

OnCheckedChangedその1
@OnCheckedChanged(R.id.toggleButton)
void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    // 何らかの処理
}

これもOnClickアノテーションのように、

OnCheckedChangedその2
@OnCheckedChanged(R.id.toggleButton)
void onCheckedChanged(ToggleButton buttonView, boolean isChecked) {
    // 何らかの処理
}

実際のCompoundButtonの型で引数を指定できたり(ここではToggleButton型)、

OnCheckedChangedその3
@OnCheckedChanged(R.id.toggleButton)
void onCheckedChanged() {
    // 何らかの処理
}

引数なしのメソッドを指定したりできます。

OnCheckedChangedその4
// コンパイルエラー
@OnCheckedChanged(R.id.toggleButton)
void onCheckedChanged(Button button) {
    // 何らかの処理
}

CompoundButton型のもしくはそのサブクラス以外を指定した場合、しっかりコンパイルエラーになってくれます。

また、

OnCheckedChangedその5
@OnCheckedChanged(R.id.toggleButton)
void onCheckedChanged(boolean isChecked) {
    // 何らかの処理
}

boolean型のみを引数にとるメソッドも指定できたり、

OnCheckedChangedその6
@OnCheckedChanged(R.id.toggleButton)
void onCheckedChanged(boolean isChecked, CompoundButton buttonView) {
    // 何らかの処理
}

boolean型とCompoundButton型の引数が、OnCheckedChangeListener#OnCheckedChangeListenerとは逆のメソッドも指定できるようです。

シンプルで使いやすく、学習コストが低い

非常に高機能なライブラリでも、覚えることが多かったり使い方が難しかったりすると、利用を躊躇してしまうこともあるかと思います。多人数で開発している場合、覚えることが多く使い方の難しいライブラリを、チームメンバーにその正しい使い方を共有・教育するのは大きなコストになりますよね。そのような場合は、導入の提案を躊躇したり周りから拒否される、そんなことがあるかもしれません。

ButterKnifeは非常にシンプルに使え、覚えることも少なく、とても導入しやすいと思います。

ここも大きな良い点だと思っています。

AndroidAnnotationsと比較して

投稿者はAndroidAnnotationsに関して、今回軽く調査しただけです。
よくAndroidAnnotationsを使われる方、もし間違いがあれば、コメントしていただけるとうれしいです。
また申し訳ありませんが、その点を踏まえて読んでいただけると嬉しいです。

「AndroidAnnotationsでも同じことできるよね。じゃあ高機能なAndroidAnnotationsで良くない?」

はい。その通りだと思います。AndroidAnnotationsを正しく利用すればViewにまつわる処理以外にも、冗長なボイラープレートコードを無くすことができると思います。もう既にAndroidAnnotationsを使いこなすことができている方は無理してButterKnifeを新たに覚えて使わなくてもいいと思います。

AndroidAnnotationsもButterKnifeもViewのインジェクションが可能です。(AndroidAnnotationsはそれ以外も可能ですね。)

ただAndroidAnnotationsは、Activityのサブクラスを自動生成して実際はそのサブクラスを使います。AndroidManifestにもサブクラスの方を指定する必要があります。com.mrstar.HogeActivityを定義したら、自動生成されたcom.mrstar.HogeActivity_をAndroidManifestに記述する必要があります。

またonCreateをoverrideせず、AndroidAnnotationsのお作法に従って記述する必要があるようです。

覚えて使いこなせばAndroidAnnotationsは非常に便利だと思います。ですが、
「findViewByIdやsetOnClickListenerのコードが非常に多くて、とりあえずこれらだけでも少なくなればコードがスッキリする。」

「もう運用中のコードで、多人数で開発していて、リファクタリングしたい。リファクタリングのコストとライブラリの導入コストはなるべく小さくしたい」
なような場合は、ButterKnifeの方が便利かと思います。

ここが残念【追記あり】

こちらの投稿でも触れられていますが、残念ながら、OnItemClickとOnItemLongClickにバグがあるようですね。(version 4.0.1)

【追記】AndroidStudioではversion 4.0.1でも動きました。Eclipseだけで発生するバグだったようです。

ちなみに実行時例外ではなくて、コンパイルエラーです。
OnItemClickアノテーションとOnItemLongClickアノテーションを用いると、そもそもコンパイルできません。

残念。

【追記】このバグはversion 5.0.0で修正されたようです。

ButterKnifeというネーミング

前述の@yyaammaaさんのgistから引用させていただきます。

最後に余談ですが、この "Butter Knife" というネーミングはとても気が利いていて良いなあと思いました。

  • Dagger (短刀) はいろいろなことができる。切ったり刺したり削ったり
  • Butter knife (バターナイフ) はパンにバターを塗ることぐらいしかできない。が、パンにバターを塗るときにはとても便利

ということで、なんでもできる(Dependency Injectorである) Dagger に対して、パンにバターを塗る(ViewをInjectする) ことしかできないけど、その用途であればとても便利なものだよ、という意味でButter Knifeという名前にしたんじゃないかなあと。

なるほど!
ButterKnifeという名前は、このライブラリにあった素敵な名前ですね。

まとめ

  • ButterKnifeという名前の通り、Viewのインジェクションにしか使えないけれど、それに関してはめちゃくちゃ使いやすいです
  • 学習コストが低いし、導入も簡単です
  • OnItemClick・OnItemLongClickにバグ有ります(version 4.0.1)(Eclipseだけでした。)
  • Eclipseで発生していたOnItemClick・OnItemLongClickが使えないバグがversion 5.0.0で直ったようです。
  • AndroidAnnotationsをすでに使いこなしている人は、無理に使う必要ないと思います

繰り返しになりますが、findViewByIdとsetOnClickListerが多くて、とにかくそれだけでもさくっとリファクタリングしたい。そういう人にはオススメです。

参考書籍

秀和システム発行 八木俊広著 Android オープンソースライブラリ

参考サイト・関連サイト