daisuke_nomura の日記

Android プログラマーな鉄道ファンのブログ

MVPにViewModelを導入した

タイトルの通りである。

ただし、ViewModelは今は、Architecture ComponentsのViewModelを指している。

将来的には、MVVMのViewModelも指すだろう。

MVP

MVPことModel-View-Presenterは、PresenterがModelに処理依頼なりデータ取得を行い、返ってきた値をViewに反映する設計パターンである。 ViewとPresenterは、Contractに定義されたインターフェースを経由してアクセスする。

全体を絵にするならこんな感じである。 PEAKS で販売中の 『Android アプリ設計パターン入門』を参考に作成した。

f:id:daisuke_nomula:20180520185520p:plain

Android界隈はMVVM(Model-View-ViewModel)が流行っているが、設計パターンの基本形として覚えておきたい。

Android アプリ設計パターン入門』 はもちろんマストバイアイテムなので、急いで注文しよう。

peaks.cc

ViewModelを導入

MVPにはない(今はArchitecture Componentsの)ViewModelを導入した。

以下の形になった。

f:id:daisuke_nomula:20180520190019p:plain

モデルから得られ、ViewModelを経由したObservableなりLiveDataは、Presenterでsubscribeすることにした。 これは、Presenterがモデルから取得したデータをViewに反映するMVPの設計に合わせるためである。 ライフサイクル周りは、View(実装はFragment)から提供している。

ViewModel導入は以下の意図があった。

  • MVVMに移行したかったが、データバインディング含めて作業量が膨大なので、一先ずViewModelを導入して少しずつ移行する
  • モデルの内部でデータをキャッシュしているものの、応答に2、3秒間掛かるため、UIに近い所でキャッシュしたかった

作業量が少なかったら、一気に移行しただろう。 目標としては、MVVM化にあたって、ワンクッション置いたのである。

レイヤーが増えた

レイヤーが増えちゃって面倒くさそうと思うだろうが、まさにその通りである。

というのも、きちんとMVPで整理されていたり、既にモデルからObservable/LiveDataを得ているなら、PresenterをViewModelに書き換えるのはそれほど苦労しないと思われる。

ワンクッション置いているので、過渡期な印象は否めない。

また、時期は不明だが、将来的な改修でPresenterはなくなり、ViewModelはMVVMのViewModelも指すだろう。

MVPVM

microsoftのウェブサイトを見ると、MVPVMという設計パターンがあるらしい。

データバインディングを使っており、ココで書いたのとは異なる。

MVPVM 設計パターン - WPF 向けのモデル - ビュー - プレゼンター - ビューモデル設計パターン

すべての画面遷移を管理するクラスを作るべきか

kgmyshin さんの「大きめのAndroidアプリでの設計を考えてみる~pocket~」に書かれているコラムで共感したし、安心した。 このコラムの題名は、「すべての画面遷移を管理するクラスを作るべきか」。

マストバイアイテムなので、今すぐBOOTHの購入ボタンを押そう。

booth.pm

簡単にまとめたい。

過去のアプリは、MVPで設計され、多くの画面にRecyclerViewが使われていた。 アイテムを選択すると、次の画面にジャンプして(=Fragmentを入れ替えて)詳細が見れるようなよくある遷移が多かった。

自分が経験した「すべての画面遷移を管理するクラス」は、だいたい以下のような機能を持っていた。

  • ActivityがonCreate時に自身を渡してくるので、シングルトンで持つ
  • Fragmentは、様々なActivityのR.id.contentに配置される
  • 同一プロセスなら、どこからでも呼べる
  • 要求されればstartActivityする

このクラスには、以下のメリットがある。

  • どこに遷移するか遷移元で分かる
  • ActivityとFragmentのペア事情を気にせずに遷移できる
  • Activityは画面遷移でほとんど何もしないので実装をサボれる

画面遷移は好みの別れる所であるが、残念ながら、これは画面数が50を超えるようなアプリには向かない。

どのFragmentからどのFragmentに遷移するか把握はできるが、どのActivityで表示されているかをコードで把握するのは難しく、 表示が予想外な時にActivity終了のパターンに突き当たる。 戻るボタン押下時に複雑になることも想像に難しくない。

それに、タブレット用レイアウトで、左側に一覧、右側に詳細画面を作成しようと考えたら、真っ先に潰す対象になる。

その他のデメリットは、ツイートしたとおり。

どう解決したか

解決方法は簡単で、古典的な方法を採った。 画面遷移はActivityに全て任せ、Activityは自身に配置されるFragmentを全て知り管理し、 FragmentやRecyclerViewのアダプタは 最終的にActivityのメソッドを呼ぶ形である。 もちろん、「すべての画面遷移を管理するクラス」は潰した。

絵にするとこんな感じ。

f:id:daisuke_nomula:20180504142217p:plain

処理の流れは以下のように変更した。

  1. RecyclerViewのアダプタのインターフェースをPresenterが実装し、呼ぶ
  2. PresenterはContractに書かれたインターフェース経由でFragmentに画面遷移を指示する
  3. Fragmentはインターフェース経由でActivityに遷移を要求する

Bundleに詰め込む値は、インターフェースの引数としてActivityまで渡していく形にした。 これで、「すべての画面遷移を管理するクラス」の「次画面に渡すパラメータを取得するためにRecyclerViewのアダプタがFragmentやPresenterを知っている」必要はなくなり、参照は簡潔になった。

正直な所、RecyclerViewのインターフェースをPresenterが実装するのには異論もあるはず。 RecyclerViewのアダプタはFragmentが知っているのだから、Fragmentが実装すべきではと思うが、 FragmentからPresenterに依頼し、またFragmentに戻ってくることが確実なので、ショートカットした。

副作用として、遷移先Fragmentと同一のものが既に表示中の場合、何もしないと行った制御も可能になった。

実装としては、Android StudioでListテンプレートを選んだ結果と近い。なので、古典的と称した。

f:id:daisuke_nomula:20180504123715p:plain

TransactionTooLargeException

これでもTransactionTooLargeExceptionは解決できない。

Bundleに詰め込む値は必要なものなので、どうしても次画面に渡さなければならない。

ココの解決策は、デカイ値をシングルトンで持つことで解決した。

SharedPreferencesにJSONで保存する方法もあるが、アプリがクラッシュした後に値を持っていてほしくない事情もあり、シングルトンで持つ形とした。 不要になったら、消す必要はある。

Fragmentを切り出す

エラーの場合の表示だったり、戻るボタン押下で複雑な遷移になりそう、かつ切り出せそうな場合は、新しくActivityを作成して切り出した。

そうすることで、特定のAcitivityだけで管理され、局所化した。

インターフェース地獄

だが、この解決方法だとJavaのインターフェース地獄になるのは間違いない。

とくに、Fragmentが様々なActivityに表示される場合、ActivityとFragmentのペア事情が分からず、 改修初期はテンプレートで生成された通りのRuntime Exceptionのログを見ることになる。

f:id:daisuke_nomula:20180504123719p:plain

一つ一つぶち当たって対処した。

タブレット用レイアウト

上でタブレット用レイアウトの話を挙げたが、todo-mvpにはタブレットでの設計もあるので参考になる。

github.com

Kii Cloud の RxJava 2 ライブラリ作った

RxKii

RxKii という Kii Cloud の RxJava 2 ラッパーライブラリを作りました。
RxJava 2 の Completable or Single or Observable を返してきます。

github.com

クラス名は、Kii のクラスに Rx を足したものになってます。
KiiObject -> RxKiiObject

コールバック地獄

Kii Cloud Android SDK は多くの API で同期と非同期の両方が揃っています。
ただ当然ながら、非同期 API を使うとコールバック地獄に陥り、ネストが非常に深くなってしまうので、 RxJava で返すようにしました。

Completable or Single

RxKii では、 Completable で返してくる API が多いです。
これは、 RxKiiUser を見ると分かります。

RxKii/RxKiiUser.java at master · daisuke-nomura/RxKii · GitHub

Completable は返す値がない場合に用いられるので、 save/update/delete は自然と Completable になります。

例外は refresh で、kiiUser.refresh(); は Single を返します。ここも Completable になるはずですが、 KiiUser を返します。 これは、 refresh 後に flatMap で次の処理につなぐだろうという想定でこうしました。

Observable を返すのは KiiUploader/KiiDownloader のみ

Observable を返すのは、 KiiUploader/KiiDownloader をラップしたRxKiiUploader/RxKiiDownloader の transferAsObservable だけです。 非同期 API が返す onProgress をラップし、onNext で進捗状況を返してきます。

RxKii/RxKiiUploader.java at master · daisuke-nomura/RxKii · GitHub

ここで見つけましたが、 Kii Cloud Android SDK の KiiRTransferCallback の onTransferCompleted の 引数 Exception に @NonNull アノテーションが付いていますが、Kii のドキュメントを読むと、この Exception は正常時には null のはずなので、RxKii では null チェックを行っています。

Object Body のアップロード

ここは @Nullable の間違いだろうと Kii のTwitterに報告済みです。

Completable を flatMap で繋ぐ際の注意点

また、 README に書いたサンプルを実行した際に気付きましたが、Completable から Single に繋ぐ際に、 Maybe を経由し、 flatMapSingle すると NoSuchElementException になるので、ご注意。

RxJava 2でNoSuchElementExceptionに遭う件 - Qiita

RxJava で重複を削除する

RxJava では、要素の重複を削除することもできます。
コードは RxJava 2 です。

distinct

distinct メソッドを使うと重複要素を削除できます。

Observable
        .just(1, 1, 2, 1)
        .distinct()
        .blockingForEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) throws Exception {
                Log.d("koko", String.valueOf(integer));
            }
        });

出力結果

koko: 1
koko: 2

distinctUntilChanged

distinctUntilChanged メソッドを使うと、次に重複しない要素が出てくるまでの間、重複が削除されます。

Observable
        .just(1, 1, 2, 1, 1, 2, 1, 1)
        .distinctUntilChanged()
        .blockingForEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) throws Exception {
                Log.d("koko", String.valueOf(integer));
            }
        });

出力結果

koko: 1
koko: 2
koko: 1
koko: 2
koko: 1

オブジェクトも可能

対象がオブジェクトでも可能です。
オブジェクトの場合、 distinct に Function を食わせ、その戻り値が一致してるものが削除されます。

class Koko {
    public String str;

    public Koko(String str) {
        this.str = str;
    }
}
List<Koko> kokoList = new ArrayList<>();
kokoList.add(new Koko("aaaa"));
kokoList.add(new Koko("bbbb"));
kokoList.add(new Koko("aaaa"));

Observable
        .fromIterable(kokoList)
        .distinct(new Function<Koko, String>() {
            @Override
            public String apply(Koko koko) throws Exception {
                return koko.str;
            }
        })
        .blockingForEach(new Consumer<Koko>() {
            @Override
            public void accept(Koko koko) throws Exception {
                Log.d("koko", String.valueOf(koko.str));
            }
        });

出力結果

koko: aaaa
koko: bbbb

参考
ReactiveX - Distinct operator

ナショジオの MARS が面白い

National Geographic の MARS が面白いです。日本語タイトルは「マーズ 火星移住計画」。
hulu で配信中です。

www.hulu.jp

channel.nationalgeographic.com

製作総指揮はロン・ハワードなので、面白いに決まってた。

有人火星探査を取り上げた特集で、2016年時点の各人へのインタビューパートと、有人火星探査を実施する2033年のドラマパートの2パートが交互に連なって構成されてます。
インタビューには SpaceXイーロン・マスクはもちろん、 The Martian (邦題: オデッセイ)の作者や NASA が登場します。

YouTube にも前回までのあらすじだったり、幼いころの主人公が登場する第0話となるドラマが公開されています。

www.youtube.com

www.youtube.com

この MARS は National Geographic の紙面連動企画で、2016年11月号に同じように、有人火星探査と移住生活の特集が組まれています。

ナショナル ジオグラフィック日本版 2016年11月号 [雑誌]

ナショナル ジオグラフィック日本版 2016年11月号 [雑誌]

2015年に成功させた SpaceX のロケット第一ステージの帰還が火星への着陸にも応用できたり、
スペースシャトルは我慢ならないとか、巨大なサターンVロケットが火星のことも考慮されていたのは知りませんでした。

www.youtube.com

サターンV - Wikipedia

南極で1年間生活できないなら火星まで持たないそうなので、僕には無理っぽいです。