Slide 1

Slide 1 text

RxJavaを1年使って見えてきたこと Naoto Nakazato

Slide 2

Slide 2 text

自己紹介 ● Naoto Nakazato ● Yahoo Japan Corporation ● Yahoo!知恵袋 ● アカウント ○ Twitter: @oxsoft ○ Facebook: naoto.nakazato ○ GitHub: oxsoft ○ Qiita: oxsoft

Slide 3

Slide 3 text

Reactiveしてますか? いまやアプリに欠かせない存在に

Slide 4

Slide 4 text

今日お話しすること ● Yahoo!知恵袋のフルリニューアルから1年経った ● RxJavaを導入したので、その影響を振り返ってみる ● 恩恵を受けていること & こうしておけば良かったこと ● 抽象的な設計論というより、具体的なコードの話 ● コードは基本的にRxJava2ではなくRxJava1がベースとなっています

Slide 5

Slide 5 text

基本構成

Slide 6

Slide 6 text

アプリの基本構成 ● 2015年11月に1からリニューアル ● 主なOSS ○ RxJava1/RxAndroid ○ Gson ○ Lombok ○ AndroidAnnotations ○ Retrolambda ○ Glide

Slide 7

Slide 7 text

アプリの基本構成 ● Application ○ Modelのインスタンスを保持 ● Model ○ データの保持(BehaviorSubject) ○ APIリクエストの組み立てと実行 ○ APIレスポンスを内部で使う Beanクラスに変換 ● Activity ○ Application経由でModelにアクセス ○ ユーザーアクションに応じて Modelにリクエスト ○ ModelからObservableを取得し表示系を更新する ● ApiRequest/ApiResponse ○ Modelのみ利用 ● Bean ○ ModelからActivityに情報を渡すのに利用 API Model Activity Request Response Bean

Slide 8

Slide 8 text

良かったこと

Slide 9

Slide 9 text

RxAndroidを使って良かった2つのこと ● http://qiita.com/oxsoft/items/9ae07c5512449b15b923 ● 2016年5月に書いた記事 ● 非同期処理が楽 ● 複数個所で最新の情報を表示

Slide 10

Slide 10 text

非同期処理が楽 public Single request(Param... params) { return Single.create((Single.OnSubscribe) singleSubscriber -> { // 非同期処理 try { singleSubscriber.onSuccess(new Result()); } catch (Exception e) { singleSubscriber.onError(e); } }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()); } API Request 通信処理(非同期処理)をRxでラップしている OSSを使えば勝手にやってくれることも多い

Slide 11

Slide 11 text

非同期処理が楽 public Single request(Param... params) { return request(...).map(result -> bean); } Model request(...).subscribe(bean -> { // 終わった後の処理 }, throwable -> { // 例外処理 }); Activity レスポンスクラスをデータクラスに変換(腐敗防止層的なこと) 適宜キャッシュを返したりする データを受け取って、表示更新処理などを行う。エラー時の挙動も書く

Slide 12

Slide 12 text

複数箇所で最新の情報を表示 private BehaviorSubject studentName = BehaviorSubject.create(); private void setName(String name) { studentName.onNext(name); } public String getName() { return studentName.getValue(); } public Observable getNameObservable() { return studentName.asObservable(); } ModelにBehaviorSubjectを保持し、観測用の関数を用意する ”観測可能な”変数のように扱える

Slide 13

Slide 13 text

doOnTerminate() for Single public static Single doOnTerminate(Single single, Action0 onTerminate) { return Single.create(singleSubscriber -> single.subscribe(value -> { onTerminate.call(); singleSubscriber.onSuccess(value); }, error -> { onTerminate.call(); singleSubscriber.onError(error); })); } 成功しても失敗しても実行する処理を書く(ObservableにあるものをSingleに実装) ※RxJava1ではSingleのlift()がprivateなので、上記のような実装をしています

Slide 14

Slide 14 text

ProgressDialogを簡単に出す public Single progress(Single single) { ProgressDialog progressDialog = new ProgressDialog(this); progressDialog.show(); return doOnTerminate(single, progressDialog::dismiss); } 以下のようにすれば、簡単にProgressDialogが表示できる progress(request(...)).subscribe(...)

Slide 15

Slide 15 text

二重をリクエスト防止する private List requests = new ArrayList<>(); protected Single singleRequest(Requestable requestable) { if (requests.contains(requestable)) { return Single.error(new DuplicateRequestException()); } requests.add(requestable); return doOnTerminate(requestable.request(), () -> requests.remove(requestable)); } ● リクエスト中でなければ、リクエストする ● リクエスト中ならば、エラーを返す

Slide 16

Slide 16 text

こうしておけば良かった

Slide 17

Slide 17 text

通信要求と通信状態は分けるべきだった Single search(String query) 以下のような関数がModelにある あるいは以下のように、リクエストと結果が分かれている場合もある Single requestProfile(int userId) Observable observeProfile(int userId)

Slide 18

Slide 18 text

通信要求と通信状態は分けるべきだった シンプルだけど…… 画面回転などをした時に通信状態が引き継げなくなる

Slide 19

Slide 19 text

通信要求と通信状態は分けるべきだった void requestProfile(int userId) Observable observeProfileRequestState(int userId) Observable observeProfile(int userId) リクエストの要求、リクエスト状態の監視、結果の監視に分けると良さそう リクエスト状態とリクエスト結果を分けているのは、 キャッシュを即座に表示しつつ、最新の情報を取得する時などに有効

Slide 20

Slide 20 text

unsubscribeする場所が良くなかった onCreateで通信を開始している それに対応する形でonDestroyでunsubscribeしていた  → GlideがIllegalArgumentExceptionで怒る  → 表示系の購読/解除はonStart/onStopが良さそう

Slide 21

Slide 21 text

unsubscribeする場所が良くなかった protected void onCreate(Bundle savedInstanceState) { model.requestProfile(userId); } protected void onStart() { model.observeProfileRequestState(userId).subscribe(requestState -> {...}); model.observeProfile(userId).subscribe(profile -> {...}); } protected void onStop() { unsubscribe(); } ※ただしDialogFragmentの表示は、onSaveInstanceStateの後だとダメなので、  タイミングをずらすなど別途対策をする必要があります

Slide 22

Slide 22 text

ストリームに流す値はimmutableにすべきだった ● オブジェクトを受け取った側で状態を変更するのがご法度なのは当たり前 ● インスタンスを使いまわすとdistinctUntilChangedなどがおかしくなる public void updateStudentName(String name) { Student student = studentSubject.getValue(); student.setName(name); // Student is mutable! studentSubject.onNext(student); } ※distinctは内部でHashSet(ハッシュ値)を使っているのでこの問題が顕在化しにくいです

Slide 23

Slide 23 text

ストリームに流す値はimmutableにすべきだった BehaviorSubjectを細かく切ると良さそう APIごとに得られる情報をベン図にし、それぞれの領域にSubjectを用意する 細かくしすぎると管理が複雑になるので、限度は必要 例えば右図では、  API1のレスポンスでXとYのSubjectに値を流し、  API2のレスポンスでYとZのSubjectに値を流す API1:ユーザのツイート一覧 API2:ユーザのプロフィール X:ツイート情報 Y:ユーザの概要 Z:ユーザの詳細      など

Slide 24

Slide 24 text

ストリームにnullを流すべきではなかった ● @NonNullや@Nullableが使えない ● でも毎回nullチェックをするのは正直アホらしい ● RxJava2に移行できない ● SingleはCompletableなどが用意されている ● Kotlinを使った方がいいかも

Slide 25

Slide 25 text

Listの更新方法を統一すべきだった 一番下までスクロールした時に追加のコンテンツを読み込む画面で、 追加分のデータしか通知しなかった Single requestMore() void requestMore() Observable observeResult() 毎回全データを返却した方が、画面回転や、ページ管理などで有利になる

Slide 26

Slide 26 text

RecyclerViewを積極的に使うべきだった NestedScrollView+LinearLayoutを使って実装を進めていくうちに、 気づいたらリストっぽいものであることに気がついた 要素の挿入や、内容の変更がとても大変になってしまった (diffを取るクラスを実装したり、各要素をObservableで管理したり……)

Slide 27

Slide 27 text

RecyclerViewを積極的に使うべきだった “要素数が不定”ならRecyclerViewの利用を考え始めたほうが良さそう 先ほどのように全データを毎回通知すれば、 DiffUtilを使うことでいい感じにアニメーションしてくれる DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(...); diffResult.dispatchUpdatesTo(adapter); // adapterに通知

Slide 28

Slide 28 text

まとめ ● 通信要求と通信状態は分ける ● unsubscribeする場所をよく考える ● ストリームにはimmutableを流す ● ストリームにはnullは流さない ● Listっぽいものの扱いに注意する ● RxJava2に移行する ● ついでにKotlin化も ご清聴ありがとうございました