AndroidでRxJava使う場合のエラーハンドリング

RxJavaのエラーハンドリングはやり方とか罠がいろいろあるので、備忘録として残します。
本記事では想定する場面として、API通信を含むストリームでエラーが起きたときのハンドリングについて書きます。

## 基本

Rxでエラーについて何かしらハンドリングしたいとなった時に、まず最初に考えるのはsubscribeのonErrorで処理することだと思います。

```kotlin
fun someApi(): Single<Int> {
    return Single.error(IOException("APIでエラーが発生"))
}

fun main() {
    someApi()
            .subscribe(
                    { println("Item = $it") }, // onSuccess
                    { println(it.message) } // onError
            )
    Thread.sleep(10000)
}
```

実行結果

```
APIでエラーが発生
```
someApiがなんらかのWebAPI呼び出しをイメージしています。
このパターンは例えば、onCreateなどでユーザーのデータを取得して画面描画するなどの場面で私は使っています。取得できなかった場合はエラーが起きた旨をユーザーに伝えて画面を開き直してもらうなどですね。

## 基本のやり方だとできないこと
全部subscribeのonErrorでエラーを処理すれば良い気がしてきますが、対応できないパターンがあります。
subscribeのonErrorでハンドリングする場合、そのストリームは`onErrorの処理を終えたら終了`してしまいます。Singleのストリームなら上記でOKですが、Observableの場合は値がN個流れてくるので、途中でエラーが起きた時にそこでストリームが終了して欲しくなければ別の方法でハンドリングする必要があります。
エラーが起きてもストリームが終了して欲しくないケースの例としては、`ユーザーが更新ボタンをクリックしたら、APIを呼び出して最新のデータを取得しにいく`みたいなパターンで、rxbindingなどを使ってクリックイベントから全てRxを使って書いてる場合です。ユーザーのクリックイベントから始まるストリームは画面を開いている間は完了して欲しくないので、subscribeのonError以外でのハンドリングを考えます。

## やり方

### retryを使う

```kotlin
fun increment(): () -> Single<Int> {
    val counter = AtomicInteger(0)
    return {
        if (counter.getAndIncrement() == 0) Single.error(IOException("APIでエラーが発生"))
        else Single.just(counter.get())
    }
}

fun main() {
    val someApi = increment()
    val buttonClick = PublishSubject.create<Unit>()
    buttonClick
            .flatMapSingle { someApi() }
            .retry()
            .subscribe(
                    { println("$it 回目のクリック") },
                    { println(it.message) }
            )

    buttonClick.onNext(Unit)
    buttonClick.onNext(Unit)
    buttonClick.onNext(Unit)
    
    Thread.sleep(10000)
}
```

```:実行結果
1 回目のクリック
3 回目のクリック
```

PublishSubjectを使って、ボタンクリックのイベントを模擬しています。
3回クリックして、2回目にAPI呼び出しでエラーが生じるように実装していますが、3回目のクリックイベントがきちんと流れてきているのが分かります。retryは無条件で即再subscribeしてくれるメソッドです。失敗したら何かメッセージを出すとかやりたい場合は、retryより前で`doOnError`を使って処理する必要があります。

### retryWhenを使う

```kotlin
fun main() {
    val someApi = increment()
    val buttonClick = PublishSubject.create<Unit>()
    buttonClick
            .flatMapSingle { someApi() }
            .retryWhen { it: Observable<Throwable> ->
                it.flatMap {
                    if (it is IOException) Observable.just(Unit)
                    else Observable.error(it)
                }
            }
            .subscribe(
                    { println("$it 回目のクリック") },
                    { println(it.message) }
            )

    buttonClick.onNext(Unit)
    Observable.timer(1, TimeUnit.SECONDS).subscribe { buttonClick.onNext(Unit) }
    Observable.timer(3, TimeUnit.SECONDS).subscribe { buttonClick.onNext(Unit) }

    Thread.sleep(10000)
}
```

retryWhenを使うと、retryよりも細かくエラー時の挙動を制御できます。retryWhenに渡すラムダ式はObservableSourceを返す必要があります。このラムダ式が返すObservableSourceが値をemitしたら再subscribeされます。上記の実装ではIOExceptionの時だけエラーを握り潰して、それ以外の時は再度エラーを投げます。基本的にエラーが起きてもストリームを終了させたくないけど、何か想定外が起きたときは終了させる、みたいな時には使えますね。あとは、ObservableSourceを返すので再subscribeのタイミングを自由自在に制御できます。timerを使って数秒待ったり、subjectを使って何かのイベントをきっかけに再subscribeさせたり。

### onErrorReturnItemを使う

```kotlin
fun main() {
    val someApi = increment()
    val buttonClick = PublishSubject.create<Unit>()
    buttonClick
            .flatMapSingle { someApi().onErrorReturnItem(-1) }
            .subscribe(
                    { println("$it 回目のクリック") },
                    { println(it.message) }
            )

    Observable.interval(0,1, TimeUnit.SECONDS)
            .subscribe {
                buttonClick.onNext(Unit)
            }

    Thread.sleep(10000)
}
```

```:実行結果
1 回目のクリック
-1 回目のクリック
3 回目のクリック
```

onReturnItemメソッドを使うと、エラーが起きたときに代替値を返してくれます。上記の例では、-1という数値を返しています。エラーが起きたときに毎回-1を返すようになるのでsubscribe内でエラーを見分けることも可能です。
注意点として、このようなケースの場合、flatMapSingleのラムダの中でonErrorReturnItemしてあげる必要があります。
親戚の`onErrorReturn`というメソッドを使えば、Throwableの中身によって返す値を変えることもできます。
例えば、以下のようにしてしまうと、エラーが流れてきた時点でストリームが終了してしまいます。
これはonErrorReturnItemが代替値を流すとともに、ストリームをcompleteしてしまうからです。

```kotlin
buttonClick
            .flatMapSingle { someApi() }
            .onErrorReturnItem(-1)
            .subscribe(
                    { println("$it 回目のクリック") },
                    { println(it.message) }
            )
```

### onErrorReturnとResult型を使う
上でも少し言及しましたが、onErrorReturnを使えばThrowableによって返す値を変えることができます。

```kotlin
fun main() {
    val someApi = increment()
    val buttonClick = PublishSubject.create<Unit>()
    buttonClick
            .flatMapSingle {
                someApi()
                        .map { Result.success(it) }
                        .onErrorReturn { Result.failure(it) }
            }
            .subscribe(
                    {
                        it.onSuccess { println("$it 回目のクリック") }
                        it.onFailure { println(it.message) } //こっちでエラーが処理される
                    },
                    { println("subscribeのonError") } //　呼ばれない
            )

    Observable.interval(0,1, TimeUnit.SECONDS)
            .subscribe {
                buttonClick.onNext(Unit)
            }

    Thread.sleep(10000)
}
```

```:実行結果
1 回目のクリック
APIでエラーが発生
3 回目のクリック
```

代替値を返してくれるonErrorReturnと、KotlinのResult型を組み合わせたやり方です。
subscribeのonNextの中でさらに、`onSuccess`と`onFailure`で分岐しています。Result型のところは成功と失敗以外にもケースがある場合は、自作のクラスなどでも対応可能です。subscribeでAPIの結果を受けての処理ができるのでこのやり方が私は好きです。

## 最後に

簡単にまとめてみましたが、オペレータを調べるとまだまだエラーハンドリング系は種類がありますね。
必要になったらその都度追記していきたいと思います。以上です。