36
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AndroidのfindViewByIdでよくやるキャストとNullチェックをスッキリなコードにしてみた

Last updated at Posted at 2016-04-06

この記事はJava言語をつかったAndroidアプリの開発に関する記事です。

もうすでにDataBindingやButter Knifeでスッキリしている方は不要な話かもしれません。

findViewById周りでいろいろと面倒ですよね。
returnがNullableなのでLint様の指摘対応したり、キャスト書いたり。
※一時的にfindViewByIdが@Nullable指定されていたためこんな実装が必要でした。(2018/07/22追記)

キャストの括弧の位置とか微妙にめんどいです。
そこでそこらへんをスッキリさせてみたいとおもいまーす。

Android3分クッキング。

Activityをスッキリ3分クッキング

↓Before

BeforeActivity.java
/** この内容は古くなっています **/
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_before);
        // え?Lint様…毎回これやんの?
        TextView tv = (TextView)findViewById(R.id.text);
        if (tv != null) tv.setText("hoge");

        Button btn = (Button)findViewById(R.id.button);
        if (btn != null) {
           btn.setOnClickListener(this);
           btn.setText("fuga");
        }
    }

※一時的にfindViewByIdが@Nullable指定されていたためこんな実装が必要でした。(2017/07/25追記)

3分後(3分かけてやるとはいってない)
チーン

↓After

AfterActivity.java
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_before);
        // こんな感じにスッキリ!気分屋Lint様怖くない!
        viewById(R.id.text, TextView.class).setText("hoge");
        viewById(R.id.button, Button.class).setText("fuga");
        viewById(R.id.button).setOnClickListener(this);
    }

こんなんなりました。これを見ると壮観。スッキリ。
Lint様の静的解析のNullチェックしてないという文句もありません。

以下のコードを使えば上のような感じにできます。

スッキリのために何をしたのか(仕込み)

3分クッキングをするためのネタの仕込みです。
以下のコードをBaseActivityみたいなクラスに定義、Activityに継承させました。そしてそれらを使うと上のコードのようにスッキリします。

BaseActivity.java
    @NonNull
    public final <T> T viewById(@IdRes int id, Class<T> clazz) {
        View v = findViewById(id);
        checkNotNull(v);
        return cast(v, clazz);
    }

キャスト不要な場合のためにこれも追加。

BaseActivity.java
    @NonNull
    public final View viewById(@IdRes int id) {
        View v = findViewById(id);
        return checkNotNull(v);
    }

findを消したのは、スッキリにしたくて何となくです!

ここで使ったcheckNotNull()の定義は以下(参考:GitHub:google/guavaのcheckNotNull())

※checkNotNull()はGoogleのサンプルコード見ているといたるところで見ますね。
Nullが来たら容赦なく落として、さらにif文が減るので可読性に貢献します。
ロジック上Nullが来るのはほぼあり得ないコードで使いたいです。
みなさん広めましょう。

※(2018/03/03追記)この記事を書いた時は不勉強だったのですが、これを契約プログラミングと言う手法で、このコードを利用するための条件を示すことで、その条件が満たされていれば正常な動作を保証するという書き方のようです。これで嬉しいのは意図しない動きを事前に防ぐことでシステムが破壊されるのをドキュメントではなくコードで防ぐことができ、メンテナンス性がすこぶる上がります。

※AndroidSDK lv19からObjectsというクラスにrequireNonNull()というメソッドが追加されていますので、minSDK=19であればrequireNonNull()に置き換えることも可能です。
参考:Objects | Android Developers

BaseActivity.java
    public static <T> T checkNotNull(T reference) {
        if (reference == null) throw new NullPointerException();
        return reference;
    }

cast()の定義は以下

BaseActivity.java
    @SuppressWarnings("unchecked") // checked !
    public static <T> T cast(View reference, Class<T> clazz) {
        if (!clazz.isInstance(reference)) throw new ClassCastException("View instance cannot cast to " + clazz.getName() + ".");
        return clazz.cast(reference);
    }

参考:GDD Blog: [Java]コードから動的にinstanceofしたい
参考2:instanceof と Class#isInstance - やさしいデスマーチ
参考3:Java/Classのキャスト, instanceof演算子の代替など - Glamenv-Septzen.net

instanceOf無しで型チェックもできるし。
Javaのキャスト構文無しでキャストできちゃいます。
しかも引数で動的にキャスト、型チェック可能なんていい感じですね。
引数のView型はObject型でも構いません。単純にわかりやすくしただけです。

こんな感じにBaseActivityにメソッドを定義しておいて、自分のActivityにいつでも使えるようにして
いい感じに使いまわすといいかもしれません。

Fragmentについても書いてみます。

Fragmentバージョン

BaseFragment.java
    @SuppressWarnings("ConstantConditions")
    @NonNull
    public final <T> T viewById(@IdRes int id, Class<T> clazz) {
        View v = getView().findViewById(id);
        checkNotNull(v);
        return cast(v, clazz);
    }

    @SuppressWarnings("ConstantConditions")
    @NonNull
    public final View viewById(@IdRes int id) {
        View v = getView().findViewById(id);
        checkNotNull(v);
        return v;
    }

FragmentだとgetView()のNullが怖いので以下も定義してUIスレッドをを使って何かするときはこれを使って書いてもらうようにします。

BaseFragment.java
   private Thread uiThread;
    private final Handler handler = new Handler();

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        uiThread = Thread.currentThread();
    }

    protected final void runOnUiThreadIgnoreState(Runnable action) {
        try {
            runOnUiThreadInternal(action);
        } catch (FragmentStateException e) {
            e.printStackTrace();
        }
    }

    // ActivityのrunOnUiThreadを参考に実装。
    private void runOnUiThreadInternal(Runnable action) throws FragmentStateException {
        checkFragmentState(this);
        if (Thread.currentThread() != uiThread) {
            handler.post(action);
        } else {
            action.run();
        }
    }

    public static Fragment checkFragmentState(@NonNull Fragment fragment) throws FragmentViewStateException{
        if(fragment.isRemoving()) throw new FragmentViewStateException("fragment is removing.");
        if(fragment.getActivity() == null) throw new FragmentViewStateException("getActivity() returned null.");
        if(fragment.getView() == null) throw new FragmentViewStateException("getView() returned null.");
        return fragment;
    }

Exceptionも定義しておきます。

FragmentViewStateException.java
class FragmentViewStateException extends Exception {
    public FragmentViewStateException(String msg) {
        super(msg);
    }
}

これを使えばたとば以下のようなコードで記述可能です。フィールドも持っていません。

MyFragment.java
    private void updateText() {
        // onDestroy後でも安心(Fragmentの状態を判定し可能ならRunnable#run()を実行させる)
        runOnUiThreadIgnoreState(new Runnable() {
            @Override
            public void run() {
                viewById(R.id.text, TextView.class).setText("hoge");
            }
        });
    }

非同期な処理でonDestroyの後にこれを呼んでも。落ちません。
runOnUiThreadIgnoreState()以外にいろいろ用意。お好みのもの使う感じで。

BaseFragment.java
    // ここまで手厚いといい感じ?
    protected final void runOnUiThread(Runnable action) throws FragmentStateException {
        runOnUiThreadInternal(action);
    }

    protected final void runOnUiThread(Runnable action, OnErrorCallback callback) {
        try {
            runOnUiThreadInternal(action);
        } catch (FragmentStateException e) {
            if(callback != null) callback.onError(e);
        }
    }

    interface OnErrorCallback {
        void onError(Object msg);
    }

はースッキリした。

まとめ

内容的にはここら辺のボイラープレートを回避するためのButter Knifeみたいなライブラリもあるので、結構ニッチだと思います。
そもそも動機は「finalにできない副作用の可能性があるフィールドをなるべく保持したくないし、カスタムViewだと何かしらリークしそう」から来ていて、目指すものが似ていてもButter Knifeとは違います。(機能衝突はしませんが解決できる問題範囲が違う事に注意してください。)
なので世に出ているライブラリが政治的や宗教的な理由で使えない人や
似たような動機を持った人に、参考になればと思います。

ここまで読んでくださった方ありがとうございました。

この記事に書いてあるコードのサンプルなどは以下のURLにあります。
https://github.com/272shin16/Android-SDK-Extensions/tree/activity-fragment-extensions





DataBinding使えばこんなの気にしなくていい感じ。少し使い方を勉強する必要があるけど。
自動生成されるBindingクラスのフィールドを保持する必要はあるけど、これはそんなにリスクなさそう。
DataBindingに関する参考記事:http://qiita.com/izumin5210/items/2784576d86ce6b9b51e6





追記(2017/07/25)

https://developer.android.com/preview/api-overview.html#fvbi-signature
に書いてある通り compileSDKを26に指定すると以下記述が不要になり

TextView text = (TextView) findViewById(R.id.text);

以下のように書けるようになりました。

TextView text = findViewById(R.id.text);

けれどワンライナーで書きたいという要求には答えている物ではないと思うので、このトリックは意外とまだ使えるかもしれません。

36
35
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?