Edited at

Android でイベントバスを使う

More than 5 years have passed since last update.


イベントバス

Android では、非同期処理の返り値はコールバックインタフェースを介してやり取りされる。

これ以外にも、Observer パターンに基いて設計されているクラス(SharedPreferencesなど)や、Activity と Fragment とのやりとりなどでも、コールバックインタフェースを定義して、その実装とライフサイクル管理をする。

一方で、機能が増えるとその分コールバックインタフェースの定義も増え、Activity が幾つものインタフェースを実装することがある。コールバックインタフェースの定義が増えてくると、その分だけ依存関係が複雑になりやすくなったり、コールバックを受けて更に非同期処理を呼び出して…としていくと、どんどんネストが深くなったりしていく(コールバック地獄)。

そこで、コールバックメソッドを呼ぶタイミングでイベントを発火し、コールバックインタフェースの実装ではなく、イベントを常時監視するメソッドを用意しておいて、発火したタイミングで呼び出されるようにする仕組みとして、EventBus を使うことで、インタフェースによる依存関係を整理したり、コールバック地獄を解消したりすることができる。


square/otto

https://github.com/square/otto

square 社が提供する EventBus の仕組み。

使い方は以下のようにする。

public final class EventBusHolder {

// EventBus のインスタンス自体はどの Activity からでも同じものを使うようにする
// DI フレームワークで Producer を使うと便利
public static final Bus EVENT_BUS = new Bus();
}

// ===========
public class SampleActivity extends Activity {
// ...

@Override
protected void onResume() {
super.onResume();
// EventBus の登録
EventBusHolder.EVENT_BUS.register(this);
}

@Override
protected void onPause() {
// 登録の解除
EventBusHolder.EVENT_BUS.unregister(this);
super.onPause();
}

// イベントハンドラの宣言
@Subscribe
public void onClickButton(ButtonClickEvent event) {
Toast.makeText(getApplicationContext(), "Button clicked " + event.getClickCount() + " times.", Toast.LENGTH_SHORT).show();
}
}

// ===========
public class SampleFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>{
private int mClickCount;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_sample, null, false);
Button button = (Button) view.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// イベントの発火
EventBusHolder.EVENT_BUS.post(new ButtonClickEvent(mClickCount++));
}
});
return view;
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
new CursorLoader(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor result) {
// 以下は上手くイベントがハンドラにわたらない(FragmentTransaction をこのコールバックで取り扱えないのと同じ理由)
EventBusHolder.EVENT_BUS.post(new CursorLoadedEvent(result));
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {}
}

// ===========
// イベントオブジェクト
public class ButtonClickEvent {
private final int mClickCount;

public ButtonClickEvent(final int clickCount) {
mClickCount = clickCount;
}

public int getClickCount() {
return mClickCount;
}
}

// ===========
// イベントオブジェクト
public class CursorLoadedEvent {
private final Cursor mCursor;

public CursorLoadedEvent(final Cursor cursor) {
mCursor = cursor;
}

public Cursor getCursor() {
return mCursor;
}
}

Google Guava をベースとしているが、いくつかの点で Guava とは異なる実装をしている。また、Android 向けに最適化されていたり、挙動に制限を加えていたりする。


  1. クラスの継承階層を辿らず、otto に登録したオブジェクトの型で宣言されている@Subscribeなメソッドのみをイベントハンドラとして登録する。


  2. @Producerなメソッドを宣言することで、予め定義したデータを、非同期処理後のイベントを待つこと無く配信できる。

  3. 基本的には、メインスレッド上でイベントを発火し、メインスレッド上でイベントを受信する設計になっている。設定次第で、メインスレッド以外からもイベントを発火可能だが、イベントを受信するハンドラ側は常にメインスレッドでなければならない。


greenrobot/EventBus

https://github.com/greenrobot/EventBus

Android に最適化された EventBus の仕組み。

考え方や使い方は otto と同じで、使用するメソッドもほぼ同じものが提供されている。

一方で、イベントハンドラの宣言やスレッドに関するところで otto と異なる点がある。


  1. クラスの継承階層を辿り、親クラスのイベントハンドラ宣言にもイベントが通知される。

  2. アノテーションではなく、メソッドの命名によってイベントハンドラの宣言をする。

  3. 異なるスレッド間どうしでイベントを通知しあうことが出来る。ただし、イベントの通知を受ける側は、その処理を実行するスレッドをブロックしないように設計する必要がある。


  4. @Producerによるイベントキャッシュの仕組みを自分で実装する必要はなく、StickyEvent として EventBus が管理してくれる。

// ===========

public class SampleActivity extends Activity {
// ...

@Override
protected void onResume() {
super.onResume();
// EventBus の登録
EventBus.getDefault().register(this);
}

@Override
protected void onPause() {
// 登録の解除
EventBus.getDefault().unregister(this);
super.onPause();
}

// イベントハンドラの宣言
public void onEvent(ButtonClickEvent event) {
Toast.makeText(getApplicationContext(), "Button clicked " + event.getClickCount() + " times.", Toast.LENGTH_SHORT).show();
}

// イベントハンドラの宣言。メインスレッド上で実行することを強制する場合
public void onEventMainThread(CursorLoadedEvent event) {

}
}

// ===========
public class SampleFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>{
private int mClickCount;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_sample, null, false);
Button button = (Button) view.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// イベントの発火
EventBus.getDefault().post(new ButtonClickEvent(mClickCount++));
}
});
return view;
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
new CursorLoader(MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor result) {
// イベントの通知
EventBus.getDefault().post(new CursorLoadedEvent(result));
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {}
}

// ===========
// イベントオブジェクト
public class ButtonClickEvent {
private final int mClickCount;

public ButtonClickEvent(final int clickCount) {
mClickCount = clickCount;
}

public int getClickCount() {
return mClickCount;
}
}

// ===========
// イベントオブジェクト
public class CursorLoadedEvent {
private final Cursor mCursor;

public CursorLoadedEvent(final Cursor cursor) {
mCursor = cursor;
}

public Cursor getCursor() {
return mCursor;
}
}


BroadcastReceiver との違い

otto, EventBus ともに、単純な Java オブジェクトを用いてイベントを通知できる。

BroadcastReceiver の場合、Intent の仕組みに準じる実装をしなければならないため、イベントオブジェクトは Parcelable である必要があったり、Extra として Intent にオブジェクトを詰め込んだり、イベントを受け取る側も、IntentFilter を宣言したり、受け取った Intent から Extra を取り出したりと、実装コストが高くなりがちだが、otto や EventBus であれば、イベントオブジェクトを単純なクラスとして宣言すれば、あとはメソッド宣言を規約に沿った形で実装するだけで済む。

また、BroadcastReceiver を使う場合、セキュリティに気を使わないと、インストールされているアプリすべてで Intent が受け取れてしまう問題がある。LocalBroadcastReceiver やパーミッションを使えば解決できるが、イベントの通知の実装に掛かるコストは同じか、パーミッションの分だけ増えることになるので、特に理由がなければ otto や EventBus を使うのが良さそう。