[Android] RxJavaでリアルタイム検索を制御する

# はじめに
現在、GoogleやTwitterなど、多くのWebサービスやスマートフォンアプリでリアルタイム検索がサポートされています。この記事では、Androidアプリでリアルタイム検索を実装するときに問題となることをRxJavaを利用することでスマートに解決することを目指します。

# リアルタイム検索実装時の問題点
素朴に考えると、`TextWatcher#onTextChanged`でユーザーの入力文字列を検索処理に渡して結果を表示するという方法でリアルタイム検索を実装できます。しかし、その方法には以下のような問題があります。

## onTextChangedが同じ文字列で2回呼ばれる問題
``` MainActivity.kt
// onCreateの中です。
editText.addTextChangedListener(object : TextWatcher {
    override fun afterTextChanged(s: Editable?) {
    }

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        Log.d(TAG, "query: ${s}")
        // ここで検索処理を実行
    }
})
```

`EditText#addTextChangedListener`に渡す`TextWatcher`の`onTextChanged`メソッドでユーザーの入力文字列を取得することができます。実際にEditTextに「a」と打ってみると

```
query: a
```

というログが出ます。しかし、1文字だと問題なかったのですが、端末（Nexus 5X Android7.1.1 のエミュレータで確認）によっては「abcd」と打つと

```
query: a
query: ab
query: ab
query: abc
query: abcd
query: abcd
```

というログが出ました。つまり、「ab」と「adcd」については文字列の変化がないはずなのに2回呼ばれてしまい、余計な検索処理が発生してしまいます。調べてみたところ、おそらくキーボードの予測変換が関係しているのではないかと思われます。
https://stackoverflow.com/a/19298614

## ユーザーの入力速度>検索速度の問題
入力文字列の変化があるたびに毎回検索処理を走らせればリアルタイム検索となります。しかし、ユーザーの入力速度が検索処理速度を上回っている場合は、無駄な検索処理が多くなります。たとえば、「aaaa」とユーザーが素早く入力した場合は、「a」「aa」「aaa」「aaaa」の全てで検索をするのではなく、最後の「aaaa」だけで検索を実行すれば十分です。

## 検索時間にばらつきがある問題
APIを利用してネットワーク通信で検索処理を実行する場合、検索文字列やネットワーク状況によって結果が返ってくるまでの時間がばらつく可能性があります。結果が返ってくる時間にばらつきがあると、結果が返ってくる順番が入れ替わってしまう可能性があります。たとえば、「a」「ab」の順で検索処理を実行したときに、「a」の検索処理に時間がかかり、「ab」「a」の順で検索結果が返ってきてしまうと、ユーザーに表示される検索結果が「a」のものになってしまいます。

# RxJavaを入れて制御する
## onTextChangedが同じ文字列で2回呼ばれる問題：distinctUntilChangedで解決
``` MainActivity.kt
val queryPublisher = PublishSubject.create<String>()

editText.addTextChangedListener(object : TextWatcher {
    override fun afterTextChanged(s: Editable?) {
    }

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        queryPublisher.onNext(s.toString())
    }
})

queryPublisher.distinctUntilChanged().subscribe({
    Log.d(TAG, "query: ${it}")
    // ここで検索処理を実行
})
```

まず、検索文字列を流す`PublishSubject<String>`を定義します。そして、`onTextChanged`メソッド内で文字列を発行します。実際に検索文字列を受けるのは最後の`subscribe`メソッドに渡す`Consumer#accept`（ラムダ式で省略されています）です。

ここでポイントとなるのが、`subscribe`の前にある`distinctUntilChanged`メソッドです。このメソッドを入れることで、同じ文字列が連続で来た場合は値を流さないようにできます。
参考：http://reactivex.io/documentation/operators/distinct.html

実際に「abcd」と入力した時のログは以下のようになります。

```
query: a
query: ab
query: abc
query: abcd
```

## ユーザーの入力速度>検索速度の問題：debounceで解決
``` MainActivity.kt
queryPublisher.distinctUntilChanged()
        .debounce(500, TimeUnit.MILLISECONDS)
        .subscribe({
            Log.d(TAG, "query: ${it}")
        })
```

ユーザーが素早く入力した時の最後の値だけ流すには`Observable#debounce`を使います。
参考：http://reactivex.io/documentation/operators/debounce.html

上記の書き方だと、ユーザーが文字を入力してから500ms経過したら値が流れるようになっています。実際にアプリで「a」を連打で入力して「aaaa」となった時点で手を止めると以下のようなログになります。

```
query: aaaa
```

また、`Observable#throttleLast`でも似たようなことが実現できます。この場合は、ユーザーが続けている途中でも一定間隔で値を流すようになります。
参考：http://reactivex.io/documentation/operators/sample.html

## 検索時間にばらつきがある問題：Disposableで解決
*次節の[`switchMap`で解決する方法](https://qiita.com/ara_tack/items/c57cf074ca06a329fad3#%E8%BF%BD%E8%A8%98-%E6%A4%9C%E7%B4%A2%E6%99%82%E9%96%93%E3%81%AB%E3%81%B0%E3%82%89%E3%81%A4%E3%81%8D%E3%81%8C%E3%81%82%E3%82%8B%E5%95%8F%E9%A1%8Cswitchmap%E3%81%A7%E8%A7%A3%E6%B1%BA)がよりスマートなので、そちらをおすすめします。*

最後に検索した結果だけを受け取れるようにすることで、問題の解決を目指します。

``` MainActivity.kt
private var searchDisposable: Disposable? = null

override fun onCreate(savedInstanceState: Bundle?) {
    // 〜省略〜

    queryPublisher.distinctUntilChanged()
            .debounce(500, TimeUnit.MILLISECONDS)
            .subscribe({
                Log.d(TAG, "query: ${it}")
                searchDisposable?.dispose()
                searchDisposable = search(query = it)
                        .subscribe({
                            // 結果の表示
                        })
            })
}

/**
 * 検索用メソッド
 */
fun search(query: String?): Observable<ArrayList<String>> {
    // 仮実装。本来は検索結果のリストを返す。
    return Observable.empty<ArrayList<String>>()
}
```

検索処理を`Observable<ArrayList<String>>`で受け取れるとした場合、`Observable#subscribe`の戻り値である`Disposable`を保持しておき、新たな検索処理を走らせる前にすでに保持している`Disposable`のキャンセルを`dispose`メソッドによって実施します。これによって、最後に検索した結果のみ表示することができます。

## [追記] 検索時間にばらつきがある問題：switchMapで解決
`switchMap`を使うと良いというコメントをいただいたので、調べてみました。

http://reactivex.io/documentation/operators/flatmap.html より
> RxJava also implements the switchMap operator. It behaves much like flatMap, except that whenever a new item is emitted by the source Observable, it will unsubscribe to and stop mirroring the Observable that was generated from the previously-emitted item, and begin only mirroring the current one.

`switchMap`は新しいアイテムが流れてきたら、以前に流れていたアイテムに関する処理を打ち切ります。これにより、検索処理に時間がかかっている場合に新たな検索処理が走ると前の検索処理をキャンセルすることができます。

``` MainAcitivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    // 〜省略〜

    queryPublisher.distinctUntilChanged()
            .debounce(500, TimeUnit.MILLISECONDS)
            .switchMap { search(it) }
            .subscribe({
                // 検索結果が流れてくる
                Log.d(TAG, "result: ${it}")
            })
}

/**
 * 検索用メソッド
 */
fun search(query: String?): Observable<ArrayList<String>> {
    // 仮実装。本来は検索結果のリストを返す。
    return Observable.empty<ArrayList<String>>()
}
```

こちらの方が前節よりも簡潔に書けていますね。

# おわりに
この記事では、Androidでリアルタイム検索を実装する時の問題点をRxJavaで解決する方法を紹介しました。今回の問題点はRxJavaを使わずに解決することも可能ですが、RxJavaの`distinctUntilChanged`や`debounce`、`switchMap`といったオペレーターを使うことで、より短く処理を書くことができると思います。
以上です。