RxJavaを使ったAndroidにおけるエラーハンドリング

これはRxJava Advent Calendar 2016の17日目の記事です。

はじめに

みなさん、RxJava使ってますか?私は個人でも仕事でもガッツリ使っています。

この記事では、RxJavaを使ったAndroidのエラーハンドリングに関して書きたいと思います。ちなみに私は「Error Handing in RxJava」というタイトルで DroidKaigi 2017に登壇予定で、この記事は発表予定の内容をざっくりとまとめたものになります。

エラーハンドリングでよく使うOperatorの紹介

RxJavaにはエラーハンドリング用に以下のようなOperatorが定義されています。

  • onError
  • onErrorReturn
  • onErrorResumeNext
  • retry
  • retryWhen

onError

これはストリーム内で投げられたExceptioinを受け取るためのOperatorです。

        Observable observable = Observable.just(0);
        observable
                .map(new Func1() {
                    @Override
                    public Object call(Object o) {
                        throw new RuntimeException();
                    }
                })
                .subscribe(new Subscriber() {
                    @Override
                    public void onCompleted() {}

                    @Override
                    public void onError(Throwable e) {}

                    @Override
                    public void onNext(Object o) {}
                });

mapでthrowしたExceptionがonErrorに渡ってきます。

onErrorReturn

これはExceptionが投げられた場合に代わりのデータを流すためのOperatorです。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .onErrorReturn(new Func1<Throwable, Integer>() {
                    @Override
                    public Integer call(Throwable throwable) {
                        return 10;
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

mapでExceptionがthrowされると、onErrorReturnが発火し、代わりのデータとして「10」が返ります。

onErrorResumeNext

これはExceptionが投げられた場合に代わりのストリームを流すためのOperatorです。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .onErrorResumeNext(new Func1<Throwable, Observable<? extends Integer>>() {
                    @Override
                    public Observable<? extends Integer> call(Throwable throwable) {
                        return Observable.just(0);
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

mapでExceptionがthrowされると、onErrorResumeNextが発火し、代わりのストリームとして「Observable.just(0)」が返ります。

retry

これはExceptionが投げられた場合にリトライするためのOperatorです。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .retry()
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

mapでExceptionがthrowされると、通常はストリームが終了しますが、retryを書くことで再度subscribeされます。上記のコードは無限にリトライされるますが、リトライ回数を設定することも可能で、通常はリトライ上限を設定した方がいいでしょう。

retryWhen

retryWhenは少し分かりにくいですが、リトライするトリガーとなるストリームを返すためのOperatorです。retryの場合はExceptionが投げられた瞬間にリトライが実行されますが、retryWhenを使うことで10秒待ってからリトライするというといった具合にリトライのタイミングを細かく制御することができます。

10秒待ってからリトライするコードは以下の通りです。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
                    @Override
                    public Observable<?> call(Observable<? extends Throwable> observable) {
                        return observable.flatMap(new Func1<Throwable, Observable<?>>() {
                            @Override
                            public Observable<?> call(Throwable throwable) {
                                return Observable.timer(10, TimeUnit.SECONDS);
                            }
                        });
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

ユースケース

Androidアプリでは以下のようなエラーハンドリングがよく用いられます。これをRxJavaを使って実装してみたいと思います。

  • Toastを表示する
  • リトライするかをユーザーに尋ねる

Toastを表示する

これは簡単ですね。onErrorでToastを表示するだけです。

        Observable observable = Observable.just(0);
        observable
                .map(new Func1() {
                    @Override
                    public Object call(Object o) {
                        throw new RuntimeException();
                    }
                })
                .subscribe(new Subscriber() {
                    @Override
                    public void onCompleted() {}

                    @Override
                    public void onError(Throwable e) {
                        Toast.makeText(getApplicationContext(), "Error", Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onNext(Object o) {}
                });

リトライするかをユーザーに尋ねる

これは先程紹介したretryWhenを使います。retryWhen内でSnackbarを用いてユーザーにリトライするかどうかを尋ねます。

        Observable<Integer> observable = Observable.just(0);
        observable
                .map(new Func1<Integer, Integer>() {
                    @Override
                    public Integer call(Integer integer) {
                        throw new RuntimeException();
                    }
                })
                .retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
                    @Override
                    public Observable<?> call(Observable<? extends Throwable> observable) {
                        return observable.flatMap(new Func1<Throwable, Observable<?>>() {
                            @Override
                            public Observable<?> call(Throwable throwable) {
                                return Observable.create(new Observable.OnSubscribe<Void>() {
                                    @Override
                                    public void call(final Subscriber<? super Void> subscriber) {
                                        Snackbar snackbar = Snackbar.make(view, "Retry?", Snackbar.LENGTH_INDEFINITE);
                                        snackbar.setAction("Yes", new View.OnClickListener() {
                                            @Override
                                            public void onClick(View view) {
                                                subscriber.onNext(null);
                                            }
                                        });
                                        snackbar.show();
                                    }
                                });
                            }
                        });
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {}
                });

まとめ

今回は以下のようなエラーハンドリング用のOperatorを紹介しました。

  • onError
    • Exceptionを受け取ることができる
  • onErrorReturn
    • Exceptioinが投げられた場合に代わりのデータを流すことができる
  • onErrorResumeNext
    • Exceptionが投げられた場合に代わりのストリームを流すことができる
  • retry
    • Exceptionが投げられた場合にリトライすることができる
  • retryWhen
    • リトライのタイミングを細かく制御することができる

上記のOperatorを踏まえて、Androidでよくあるエラーハンドリングの実装例を紹介しました。

  • Toastを表示する
    • onErrorでToastを表示する
  • リトライするかどうかをユーザーに尋ねる
    • retryWhenとSnackbarを組み合わせる

この他にもコメントなどいただければ実装例を追記していきたいと思います。

それでは、良いRxJavaライフを!