はじめに
この記事はポエムではありません。リアクティブプログラミングなんて仰々しい呼び名が付いていますが、イベントを流す側と受ける側がいて、イベントをどのように流すか指定することができるくらいの理解で良いと思います。
RxやFRPで検索すると難しいメタファによるたとえ話や、掴みどころのない抽象的な話が出てきて調べるほどに混乱してしまいます。私も詳しいことは分からないので、この記事ではRxとは何かということには触れずに、ただUIの仕様を眺め、その実装パターンを見るというのがこの記事の主旨です。
ケース1: SearchView
仕様
- 入力されたクエリから候補のリストを表示します
- 無駄なリクエストを送らないために入力イベントから300ms待ったあとの入力をクエリとして送信します
- 入力エリアの左側にはクエリを削除するボタンを置きます
- 入力エリアが空のときに削除ボタンは非表示になります
スクリーンショット
サンプルアプリなので候補のリストは適当な単語を表示しています。
実装
まずレイアウトですが、扱いやすくするためにToolbarとEditTextを内包したカスタムビューを作ります。
public class SearchView extends FrameLayout {
private void setup() {
View.inflate(getContext(), R.layout.view_search, this);
...
}
}
このようにして使いたい箇所にSearchViewを置くだけで機能するようにします。
<rejasupotaro.sample.view.components.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/spacing_medium" />
次にインタフェースをイメージします。
イベントが欲しい人は何もせずに300ms待ったあとのクエリが流れてくると便利そうです。
// イベントが欲しい人.java
searchView.observe()
// 使う側は流れてきたクエリを処理するだけ
.subscribe(this::updateSuggestionList);
削除ボタンを表示するかどうかはSearchView自身が管理すると良さそうです。さて、300msのウェイトと合わせるとSearchViewのObserveメソッドは以下のようになります。
// SearchView.java
public Observable<String> observe() {
// SearchView自身がイベントが欲しい人に対してイベントを流す
return Observable.create(this)
.map(query -> {
// 入力されるたび空かどうかの判定を行って
// 削除ボタンの表示/非表示を切り替える
if (TextUtils.isEmpty(query)) {
clearButton.setVisibility(View.GONE);
} else {
clearButton.setVisibility(View.VISIBLE);
}
return query;
})
// ここで300ms待つようにしてイベントを間引く
.debounce(DEBOUNCE_WAIT, TimeUnit.MILLISECONDS, Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
SearchViewからイベントを流すことができるようにOnSubscribeを実装します。
callはイベントが欲しい人が来るたびに呼ばれるメソッドです。callの引数のsubscriberがイベントが欲しい人にあたります。ここではイベントが欲しい人が来たらTextChangedListenerと繋ぐという仕事をしています。
public class SearchView extends FrameLayout implements OnSubscribe<String> {
@Override
public void call(Subscriber<? super String> subscriber) {
Assertions.assertUiThread();
final TextWatcherAdapter listener = new TextWatcherAdapter() {
@Override
public void onTextChanged(String text, int length) {
subscriber.onNext(text);
}
};
final Subscription subscription = AndroidSubscriptions
.unsubscribeInUiThread(() -> queryInput.removeTextChangedListener(listener));
queryInput.addTextChangedListener(listener);
subscriber.add(subscription);
}
}
このケースでは、イベントが欲しい人とイベントが起こるものの関係が1対1で、両者の間に処理を挟むというパターンでした。
ケース2: 複数のビューのフォーカス
仕様
- 入力欄のリストがあって、一行の中に複数の入力エリアがあります
- リストの表は右にある削除ボタンで削除することができます
- 行の中のいずれかのビューにフォーカスがあるときのみ削除ボタンを表示します
スクリーンショット
実装
今回はまず、普通にリスナーを使った実装を考えてみましょう。いずれかのEditTextがフォーカスを持っているときに削除ボタンの表示/非表示を切り替えれば良いので、OnFocusChangeを拾います。
// リストアイテム.java
editText1.setOnFocusChangeListener((v, hasFocus) -> {
updateDeleteButtonVisibility(hasFocus || editText2.hasFocus());
});
editText2.setOnFocusChangeListener((v, hasFocus) -> {
updateDeleteButtonVisibility(hasFocus || editText1.hasFocus());
});
private void updateDeleteButtonVisibility(boolean hasFocus) {
if (hasFocus) {
deleteButton.setVisibility(View.VISIBLE);
} else {
deleteButton.setVisibility(View.INVISIBLE);
}
}
これをRxで書くなら、2つのイベントを合体させて表示すべきかという新しいイベントを流すと良さそうです。
// リストアイテム.java
Observable<Boolean> o1 = Observable.create(new OnSubscribeViewFocus(editText1, true));
Observable<Boolean> o2 = Observable.create(new OnSubscribeViewFocus(editText2, true));
Observable.combineLatest(o1, o2, (f1, f2) -> f1 || f2)
.subscribe(hasFocus -> {
if (hasFocus) {
removeButton.setVisibility(View.VISIBLE);
} else {
removeButton.setVisibility(View.INVISIBLE);
}
});
この実装の良いところは、あとで仕様が変わってビューが増えたり表示条件が変わっても柔軟に対応できるところです。
このケースでは、イベントが欲しい人とイベントが起こるものの関係が1対多で、複数の条件をまとめるというものでした。
ケース3: メディアプレーヤー
仕様
- 再生ボタンを押すとメディアプレーヤーの再生の準備が始まります
- メディアプレーヤーは
停止->準備中->再生中->停止
のような状態の変化の仕方をします - メディアプレーヤーの状態の変化に合わせて再生ボタンの表示の切り替え、通知の切り替えを行います
- メディアプレーヤーは再生ボタン、通知、ロックスクリーン、ツールバーから再生と停止を行うことができます
スクリーンショット
サンプルアプリなので再生ボタンを押すと3秒間準備中になり、その後3秒間再生中になって停止になるようにしています。
実装
NotificationManagerやPlayButtonなど、複数のモジュールがMediaPlayerの状態を監視しています。
その方向への監視は、これまでのようにOnSubscribeを作って、イベントが欲しい人がそれを見るような感じになるので省略します。ここではMediaPlayerの多数のモジュールから状態が変化させられるのがポイントになります。
では、上の図の矢印を反対方向に向けて相互に監視し合うのが良いのでしょうか?
ここで、Jake WhartonのGoogle+でのある投稿について見てみます。
Thomas BruyelleJan 23, 2014
+Jake Wharton I noticed you don't use Otto, probably because you use RxJava to dispatch events.
What are the benefits of this choice ?
Jake WhartonJan 23, 2014
+Thomas Bruyelle I don't have any event-based indicators in this app. It really depends on the use case whether I'd go for using Otto or RxJava. I may even use both in the same app for different use cases.
The semantics of event distribution is different.
Otto will call subscribers for not only people listening to the class, but to any base classes or interfaces. This means I could send a String as an event and it would notify somebody that was subscribed to CharSequence. Otto also has explicit prevention of re-entrant events (a subscriber posts and event) which prevents potential infinite recursion.
RxJava, on the other hand, wants a specific Subject for each event type that you explicitly inject and subscribe to. This is nice because you inject the Subject to only hear about location updates. You can also use the functional pipeline to hook onto these events and automatically trigger other events or actions just by subscribing them to each other. Where you might see problems is if you want to listen to a lot of things. If you have 10 different Subjects you want to listen to that's 10 injections and 10 subscriptions that you have to maintain.
So depending on how you are going to create and use events you might choose one over the other.
RxJavaとOttoを比べたときの優位点について質問が来て、JakeはRxJavaはObserverパターンで、OttoはPub-subパターンであり、イベントの扱うセマンティクスが違うから、ユースケースに合わせて使い分けていると答えていました。
最初に読んだときはあまりピンと来なかったのですが、この実装を書いているときにこの投稿のことを思い出して腑に落ちました。
このケースでは、イベントが欲しい人たちは他の誰でもなく "MediaPlayerの" イベントが欲しいのでObserverで監視を行って、一方MediaPlayerは呼び出し元がどうあれイベントが欲しいので、PublishされたイベントをSubscribeするようにします。こうすることでMediaPlayerに手を加えずに呼び出し元を追加したり変更することができるようになります。
まとめ
AndroidではRxのスケジューラーを使った非同期が便利という風潮ですが、ビューのバインディングにも便利だということを言いたかったのでした。
しかし、ここまで見てきたように、Rxを入れたからといってすべてのクリックイベントをObservableにすべきかというとそうではなく、状況に応じてListenerとObserverとPub-subを使い分けるのが良いです。ここに書いたパターンがすべてではありません。どういう場面で何を使うべきかはユースケースに依存するので、Jakeが言うようにたくさんコードを書くしかないとのことです。