Edited at

RxBindingを使ってViewイベントをスッキリ制御しよう

More than 3 years have passed since last update.

今回は、RxBindingというJake先生作のViewイベントをRxJavaで扱えるようにするライブラリを使って、Viewイベントをキレイに制御していく方法を紹介していきたいと思います。

皆さんが、一度は悩んだことがありそうな事柄を中心にRxBindingを使うとキレイに処理できるものを紹介していこうと思います。


下準備

今回使用するのは、こちらのライブラリです。

それぞれ、ライブラリを追加しましょう

今回は、RxBindingは標準のものだけで大丈夫です。もしsupportライブラリのViewなども使用する場合や、Kotlinでキレイに使いたい場合はそれぞれ依存関係を追加してください。


app/build.gradle

compile 'io.reactivex:rxjava:1.1.4'

compile 'io.reactivex:rxandroid:1.2.0'
compile 'com.jakewharton.rxbinding:rxbinding:0.4.0'

※また、今回はライフサイクルイベントへのバインディングなどの処理は省きますので、お使いのライブラリに合わせて追加してください。

以上で下準備は終了です!

ここから具体的なRxBindingを使った処理について見ていきましょう


EditTextでEmailとPasswordの入力状態監視

まずは、ログイン画面などでよくあるEditTextの入力状態によってボタンのenableを切り替える処理です。


LoginActivity.java

public class LoginActivity extends AppCompatActivity {

// Regex patterns
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9-._/+?]+@[A-Za-z0-9-_]+.[A-Za-z0-9-._]+$");
private static final Pattern PASSWORD_PATTERN = Pattern.compile("^[a-zA-Z0-9]{8,20}$");

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

// FindViews
AutoCompleteTextView emailEditText = (AutoCompleteTextView) findViewById(R.id.email);
EditText passwordEditText = (EditText) findViewById(R.id.password);
Button signinButton = (Button) findViewById(R.id.signin_button);

Observable<Boolean> emailObservable = observePatternTextChange(emailEditText, EMAIL_PATTERN);
Observable<Boolean> passwordObservable = observePatternTextChange(passwordEditText, PASSWORD_PATTERN);
// 上の2つの入力状態の監視結果に変更があった場合に通知が来るようにcombineLatestでまとめて監視を行います
Observable.combineLatest(emailObservable, passwordObservable,
new Func2<Boolean, Boolean, Boolean>() {
@Override
public Boolean call(Boolean email, Boolean password) {
return email && password;
}
})
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(RxView.enabled(signinButton));

signinButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(LoginActivity.this, "ログイン処理", Toast.LENGTH_SHORT).show();
}
});
}

/**
* EditTextなどTextViewの入力の状態を正規表現のPatternと比較して、パターンをみたいしているかBooleanのObservableで結果を返すメソッド
*/

private Observable<Boolean> observePatternTextChange(TextView textView, final Pattern pattern) {
return RxTextView.textChanges(textView).map(new Func1<CharSequence, Boolean>() {
@Override
public Boolean call(CharSequence charSequence) {
return pattern.matcher(charSequence).find();
}
});
}

}


重要なのは、combineLatestで2つのEditTextの入力状態をまとめて監視する部分になります。ここで、どっちの入力状態に変化があった場合でも片方の直前の状態を引数として返してくれるため、入力状態をまとめて監視することができるようになります。

また、EditTextの監視やButtonのEnableを変更する処理はUIスレッドでやらなければいけないため、subscriveOnとobserveOnでスレッドをメインスレッドに指定しておきましょう!


Buttonの2重押し防止

次に、Buttonを押した際にButtonが2回押されるのを防止したり、複数のボタンを同時押しした際にどっちの処理も実行されるといったことが無いように制限を設ける処理です。


MainActivity.java

public class MainActivity extends AppCompatActivity {

Button mLoginButton;
Button mListButton;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLoginButton = (Button) findViewById(R.id.login_button);
mListButton = (Button) findViewById(R.id.list_button);
}

@Override
protected void onResume() {
super.onResume();
// ボタンをクリックした時の処理を指定します. clicksはVoidのObservableで返ってくるためdoOnNextで処理を予め設定しておきます
Observable<Void> loginClickObservable = RxView.clicks(mLoginButton).doOnNext(getIntentAction(LoginActivity.class));
Observable<Void> listClickObservable = RxView.clicks(mListButton).doOnNext(getIntentAction(ListActivity.class));
// 二つの処理の合計実行回数にリミットをかける
Observable.merge(loginClickObservable, listClickObservable)
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(AndroidSchedulers.mainThread())
.limit(1)
.subscribe();
}

private Action1<Void> getIntentAction(final Class<? extends Activity> clazz) {
return new Action1<Void>() {
@Override
public void call(Void aVoid) {
startActivity(new Intent(getApplicationContext(), clazz));
}
};
}

}


今回は、例としてただIntentを行うだけの処理について処理するようにしました。

こちらはlimitを設けたため、一度画面遷移した後に再度Limitを設定し直すためにonResumeの中でViewのBindを行うのが肝になります。

また、こちらは少し使い勝手の低い部分なのですがRxView.clicksではVoidのObservableが返ってくることに注意が必要です。

もし本来のリスナーと同じくViewを返したい場合はmapでViewを返してあげるのもいいかもしれません。


ListViewのスクロールイベント通知

最後に、ListViewのスクロールによってページングでリクエストを行う例です。


ListActivity.java

public class ListActivity extends AppCompatActivity {

private static final int LIMIT = 30;

private ArrayAdapter<String> mAdapter;
private boolean mIsRequest = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list);
ListView listView = (ListView) findViewById(R.id.listview);
mAdapter = new ArrayAdapter<>(getApplicationContext(), R.layout.item_list);
listView.setAdapter(mAdapter);

RxAbsListView.scrollEvents(listView)
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
// スクロール位置が下から3つ目までスクロールしたかを見る
.filter(getEndScrollFilter(3))
// リクエスト処理を行っているかどうが処理をブロックします
.filter(new Func1<AbsListViewScrollEvent, Boolean>() {
@Override
public Boolean call(AbsListViewScrollEvent scrollEvent) {
return !mIsRequest;
}
})
// データ取得のObservableに処理を繋げます
.flatMap(new Func1<AbsListViewScrollEvent, Observable<List<String>>>() {
@Override
public Observable<List<String>> call(AbsListViewScrollEvent scrollEvent) {
return getDataObservable(scrollEvent.totalItemCount(), LIMIT);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<List<String>>() {
@Override
public void call(List<String> strings) {
mIsRequest = false;
mAdapter.addAll(strings);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
mIsRequest = false;
Toast.makeText(getApplicationContext(), "失敗しました", Toast.LENGTH_SHORT).show();
}
});
}

private Func1<AbsListViewScrollEvent, Boolean> getEndScrollFilter(final int space) {
return new Func1<AbsListViewScrollEvent, Boolean>() {
@Override
public Boolean call(AbsListViewScrollEvent scrollEvent) {
return scrollEvent.firstVisibleItem() + scrollEvent.visibleItemCount() + space >= scrollEvent.totalItemCount();
}
};
}

private Observable<List<String>> getDataObservable(final int offset, final int limit) {
mIsRequest = true;
final List<String> data = new ArrayList<>();
for (int i = 0; i < limit; i++) {
data.add(String.valueOf(i + offset));
}
return Observable.just(data);
}

}


ListViewなどのスクロールイベントを扱うには、RxAbsListView.scrollEventsを使用します。

ここで、ListViewの処理で毎度おなじみのvisibleItemCountなどの処理を行いますが、これはRxのFilterを使うことでシンプルに書くことができます。また、データを処理するのにflatMapを使ってそのまま処理を繋げるようにしましょう!


最後に

どうですか?UIの処理もRxで扱うと変数の数を圧倒的に少なくできるのでキレイなのではないかと思います。

ぜひ試してみてください。

今回のソースコードはGitHubに上がっていますのでそちらも合わせてみてみてください。

GitHub RxBindingSample