Android
BroadcastReceiver
LocalBroadcastManager

イベントの通知にインテントのブロードキャストを使う話

More than 3 years have passed since last update.

ActivityやFragmentをまたいだイベントの通知を行うのは、なかなかにやっかいです。

EventBusを使うという手もありますが、ここでは、Androidが標準で持っているインテントのブロードキャストを使ってみることにします。

LocalBroadcastManager

ブロードキャストを使うと言っても、Android OSが持っているブロードキャストと、サポートライブラリのLocalBroadcastManagerが提供しているそれの2種類があるので、ケースに応じて使い分けましょう。

LocalBroadcastManagerはLocalと付いていることからわかるように、自分のアプリの外には影響しませんし、外からの影響も受けません。
ただし、その実装は、(おそらく)EventBusなどと同様に、シングルトンなstatic変数でレシーバーやイベントを管理して配信しているライブラリで、そのインターフェースをOSの提供しているブロードキャストに似せているものに過ぎません。

アンチstatic変数派で、アプリの外の影響は気にしないというのであれば、本来のブロードキャストを使った方がよいかもしれません。

BroadcastReceiverはめんどくさい

BroadcastReceiverを用いたインテントのブロードキャストは、もともと、アプリ間も含めたイベント通知の仕組みなので、とても汎用的にできています。
必要な情報をインテントに詰め込む必要があったり、インテントフィルターを指定したりといった余計な手間がかかるため、しばしばEventBusと比較して面倒と言われてしまいます。

なのであれば、面倒なところは、BroadcastReceiverの派生クラス内に隠蔽してしまうのが、まっとうなオブジェクト指向というものでしょう。

では実装の例示といきましょう

いきなりですが、以下のようなクラスを作ります。

public class MyBroadcastReceiver extends BroadcastReceiver {

    private static final String ACTION_INVOKED = "パッケージ名とかを混ぜ込んでユニークな文字列にする.ACTION_INVOKED";
    private static final String ID = "ID";
    private static final String COUNT = "COUNT";

    public interface Callback {
        void onEventInvoked(long id, int count);
    }
    // onReceive()メソッドだと何をしているのかわからないので、
    // onReceive()からコールバックを呼ぶようにする(名前大事!)

    private Callback callback;
    private LocalBroadcastManager manager;

    private MyBroadcastReceiver(@NotNull Context context, @NotNull Callback callback) {
        super();
        this.callback = callback;
        manager = LocalBroadcastManager.getInstance(context.getApplicationContext());

        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_INVOKED);

        manager.registerReceiver(this, filter);
    }

    public static MyBroadcastReceiver register(@NotNull Context context, @NotNull Callback callback) {
        return new MyBroadcastReceiver(context, callback);
    }
    // コンストラクタでregisterReceiver()しているのだけど、
    // そんなこと想像しづらいので、コンストラクタはprivateにして、
    // register()というファクトリメソッドを用意する(名前大事!)

    @Override
    public void onReceive(@NotNull Context context, @NotNull Intent intent) {
        String action = intent.getAction();

        if (ACTION_INVOKED.equals(action) {
            long id = intent.getLongExtra(ID, 0);
            int count = intent.getIntExtra(COUNT, 0);
            callback.onEventInvoked(id, count);
        }
    }
    // onReceive()内でインテントから必要な情報を引っ張り出して、コールバックを呼ぶ。

    public static void sendBroadcast(@NotNull Context context, long itemId, int count) {
        Intent intent = new Intent(ACTION_CHEERED);
        intent.putExtra(ID, itemId);
        intent.putExtra(COUNT, count);

        LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context.getApplicationContext());
        manager.sendBroadcast(intent);
    }
    // 必要な情報をインテントに詰め込んで、sendBroadcast()するクラスメソッドを用意しておけば、イベントの配信も簡単。
    // onReceive()メソッドの動作と合わせて、完全にインテントの存在を隠蔽してます。

    public void unregister() {
        manager.unregisterReceiver(this);
    }
}

と、こんな感じで実装しておけば、イベントを受ける側としては、

    
    MyBroadcastReceiver receiver;
    
    receiver = MyBroadcastReceiver.register(context, new MyBroadcastReceiver.Callback {
        @Override
        void onEventInvoked(long id, int count) {
            // なにかやる
        }
    });
    
    //  いらなくなったら
    receiver.unregister();
    

と、これだけでよくなります。
ちなみに、この手のレシーバーは単一目的ごとに作って、Callbackインターフェースのメソッドは一つにしておくのがオススメです。
AndroidStudioやIntelliJのエディタは親切なので、ごちゃっとしたところをたたんで、

    
    MyBroadcastReceiver receiver;
    
    receiver = MyBroadcastReceiver.register(context, (long id, int count)  {
            // なにかやる
    });
    
    //  いらなくなったら
    receiver.unregister();
    

と、表示してくれます。可読性が高いですね。

イベントを送る側はもっと簡単で、

    MyBroadcastReceiver.sendBroadcast(1L, 100);

これだけ。EventBusをそのまま使うよりも楽ちんです。

LocalBroadcastManagerを使わない場合は、manager変数がいらないので、コンストラクタ内とunregister()メソッドを、

    private MyBroadcastReceiver(@NotNull Context context, @NotNull Callback callback) {
        super();
        this.callback = callback;

        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_INVOKED);

        context.registerReceiver(this, filter);
    }

    public void unregister(Context context) {
        if (context != null) {
            context.unregisterReceiver(this);
        }
    }

と、しておくのがいいかと思います。フラグメントから呼ぶ場合に、ContextとしてgetActivity()なんかを使っていると、Activityが破棄されてたときなんかにトラブルのを防げます(その代わり、メモリリークしますが)。

任意のオブジェクトを渡したい

インテントにまとまった情報の塊を渡したい場合は、Parcelableインターフェースを実装しておかなければならないというのも、嫌われる原因になっていると思うのですが、Parcelableはそんなに難しくないです。

ガンガンParcelableで渡しちゃいましょう。

まとめ

・Androidに初めから用意されているから安心
・LocalBroadcastManagerを使うかどうかは、考え方次第
・面倒なところは隠蔽できます
・Parcelableは怖くない
・名前とか可読性とか大事