2018年振り返り
はじめに
2017年は忙しかったが、2018年はもっと忙しかった。今年もAndroidの一年間だった。 ただし、スプラトゥーン2もプレイした。
スプラトゥーン
ゼルダは2017年の12月にクリアしたので、2018年はスプラトゥーン2ばかりプレイしていた。 今はランク80になった。 たまに動画を投稿してる。
このスナイパー上手すぎでしょ #Splatoon2 #スプラトゥーン2 #NintendoSwitch pic.twitter.com/EjLmuxodsC
— Daisuke Nomura (@daisuke_nomura) 2018年12月23日
激しかった #Splatoon2 #スプラトゥーン2 #NintendoSwitch pic.twitter.com/aGt6TpC92K
— Daisuke Nomura (@daisuke_nomura) 2018年11月25日
3人に狙われたやつ #Splatoon2 #スプラトゥーン2 #NintendoSwitch pic.twitter.com/z9CrVjIQOu
— Daisuke Nomura (@daisuke_nomura) 2018年12月24日
Androidアプリ開発
Androidアプリ開発のインプットは多彩だった。 2017年はModel-View-Presenterが云々の状況で、Kotlinはもちろん、DIも何それ状態だった。2018年は設計・DI、主要なライブラリの組み合わせ方と、テストについて学べたので、その点は満足。
以下の言語やライブラリ、APIを使い、MVVMでリポジトリパターンな実装ができた。
- Kotlin
- Dagger2
- Room
- OkHttp
- Retrofit
- RxJava with RxKotlin
- RxRelay
- RxProperty
- Camera2 API
Dagger2を使い始めた当初は頭の中がはてなマークだらけだったが、慣れてくると非常に便利で、DIがないときはどうやってインスタンス渡していたのだろうと思ってしまう。
Camera2 APIは冬になってから苦しめられた。むつかしいね。
2017年と比べたら増えたが、技術的なアウトプットは乏しい。記事が6つしかない。 自分の経験としてはこうだったという感じのものを少し書いた程度。
MVVMでリポジトリパターンなサンプルを公開した
MVVMでリポジトリパターンなアプリを組むときに、こんな実装するよねというサンプルコードを公開した。 Dagger2やRoomも使っている。 github.com
誰もViewModel層のdoOnSuccessなどのdo*******メソッドで値を書き換えるなんて教えてくれなかったが、いつの間にか学習した。
役割的なもの
2018年の後半は、社内でリーダー的な立場で仕事をしていた。
色々と周囲に迷惑をかけてしまったが、「どうこうしたい」というのを他人に伝えるのが難しいことがよく理解できた。
あと、iOSむつかしいね。
旅行
忙しかったため、2018年はまだ夏休みが取れておらず、どこにも旅行できなかった。残念。 次はNYに行こうかと考えてはいるが、冬の間は行けない。。。
さいごに
2019年はどうなるんでしょうね。とりあえず、DroidKaigi行きます。
MVPにViewModelを導入した
タイトルの通りである。
ただし、ViewModelは今は、Architecture ComponentsのViewModelを指している。
将来的には、MVVMのViewModelも指すだろう。
MVP
MVPことModel-View-Presenterは、PresenterがModelに処理依頼なりデータ取得を行い、返ってきた値をViewに反映する設計パターンである。 ViewとPresenterは、Contractに定義されたインターフェースを経由してアクセスする。
全体を絵にするならこんな感じである。 PEAKS で販売中の 『Android アプリ設計パターン入門』を参考に作成した。
Android界隈はMVVM(Model-View-ViewModel)が流行っているが、設計パターンの基本形として覚えておきたい。
『Android アプリ設計パターン入門』 はもちろんマストバイアイテムなので、急いで注文しよう。
ViewModelを導入
MVPにはない(今はArchitecture Componentsの)ViewModelを導入した。
以下の形になった。
モデルから得られ、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という設計パターンがあるらしい。
データバインディングを使っており、ココで書いたのとは異なる。
すべての画面遷移を管理するクラスを作るべきか
「すべての画面遷移を管理するクラスを作るべきか」で作って苦しんだ話、きちんと文章にしたいな
— Daisuke Nomura (@daisuke_nomura) 2018年5月2日
kgmyshin さんの「大きめのAndroidアプリでの設計を考えてみる~pocket~」に書かれているコラムで共感したし、安心した。 このコラムの題名は、「すべての画面遷移を管理するクラスを作るべきか」。
読んだら、過去どんな所からでも次画面にジャンプできるNavigatorクラスというので苦しんだ自分の考えと一致してて共感したし安心した https://t.co/X9FcIZg9Ef
— Daisuke Nomura (@daisuke_nomura) 2018年5月2日
コラム「すべての画面遷移を管理するクラスを作るべきか」の時点で超オススメです 大きめのAndroidアプリでの設計を考えてみる~pocket~ | kgmyshin https://t.co/3O8bxTTMXH #booth_pm
— Daisuke Nomura (@daisuke_nomura) 2018年5月2日
マストバイアイテムなので、今すぐBOOTHの購入ボタンを押そう。
簡単にまとめたい。
過去のアプリは、MVPで設計され、多くの画面にRecyclerViewが使われていた。 アイテムを選択すると、次の画面にジャンプして(=Fragmentを入れ替えて)詳細が見れるようなよくある遷移が多かった。
自分が経験した「すべての画面遷移を管理するクラス」は、だいたい以下のような機能を持っていた。
- ActivityがonCreate時に自身を渡してくるので、シングルトンで持つ
- Fragmentは、様々なActivityのR.id.contentに配置される
- 同一プロセスなら、どこからでも呼べる
- 要求されればstartActivityする
このクラスには、以下のメリットがある。
- どこに遷移するか遷移元で分かる
- ActivityとFragmentのペア事情を気にせずに遷移できる
- Activityは画面遷移でほとんど何もしないので実装をサボれる
画面遷移は好みの別れる所であるが、残念ながら、これは画面数が50を超えるようなアプリには向かない。
どのFragmentからどのFragmentに遷移するか把握はできるが、どのActivityで表示されているかをコードで把握するのは難しく、 表示が予想外な時にActivity終了のパターンに突き当たる。 戻るボタン押下時に複雑になることも想像に難しくない。
それに、タブレット用レイアウトで、左側に一覧、右側に詳細画面を作成しようと考えたら、真っ先に潰す対象になる。
その他のデメリットは、ツイートしたとおり。
自分の場合、「すべての画面遷移を管理するクラスを作るべきか」で作った場合の問題は、4つあった記憶
— Daisuke Nomura (@daisuke_nomura) 2018年5月2日
1. 次画面に渡すパラメータを取得するためにRecyclerViewのアダプタがFragmentやPresenterを知っている
2. アダプタのonClickイベント処理に、Presenterが関与できない
3. 管理クラス内で完結させようとして、パラメータをBundle詰め込んでしまい、TransactionTooLargeExceptionになる
— Daisuke Nomura (@daisuke_nomura) 2018年5月2日
4. 管理クラス or Fragmentが戻るボタンをハンドリングして複雑化する
あともう1つ、最大の問題はActivityとFragmentが疎結合すぎて表示中のFragmentをActivityが感知せず、(画面遷移もそうだけど、)この画面表示中の場合はこれこれ機能動かす?止めとく?という動作ができなかったこと
— Daisuke Nomura (@daisuke_nomura) 2018年5月2日
どう解決したか
解決方法は簡単で、古典的な方法を採った。 画面遷移はActivityに全て任せ、Activityは自身に配置されるFragmentを全て知り管理し、 FragmentやRecyclerViewのアダプタは 最終的にActivityのメソッドを呼ぶ形である。 もちろん、「すべての画面遷移を管理するクラス」は潰した。
絵にするとこんな感じ。
処理の流れは以下のように変更した。
- RecyclerViewのアダプタのインターフェースをPresenterが実装し、呼ぶ
- PresenterはContractに書かれたインターフェース経由でFragmentに画面遷移を指示する
- Fragmentはインターフェース経由でActivityに遷移を要求する
Bundleに詰め込む値は、インターフェースの引数としてActivityまで渡していく形にした。 これで、「すべての画面遷移を管理するクラス」の「次画面に渡すパラメータを取得するためにRecyclerViewのアダプタがFragmentやPresenterを知っている」必要はなくなり、参照は簡潔になった。
正直な所、RecyclerViewのインターフェースをPresenterが実装するのには異論もあるはず。 RecyclerViewのアダプタはFragmentが知っているのだから、Fragmentが実装すべきではと思うが、 FragmentからPresenterに依頼し、またFragmentに戻ってくることが確実なので、ショートカットした。
副作用として、遷移先Fragmentと同一のものが既に表示中の場合、何もしないと行った制御も可能になった。
実装としては、Android StudioでListテンプレートを選んだ結果と近い。なので、古典的と称した。
TransactionTooLargeException
これでもTransactionTooLargeExceptionは解決できない。
Bundleに詰め込む値は必要なものなので、どうしても次画面に渡さなければならない。
ココの解決策は、デカイ値をシングルトンで持つことで解決した。
SharedPreferencesにJSONで保存する方法もあるが、アプリがクラッシュした後に値を持っていてほしくない事情もあり、シングルトンで持つ形とした。 不要になったら、消す必要はある。
Fragmentを切り出す
エラーの場合の表示だったり、戻るボタン押下で複雑な遷移になりそう、かつ切り出せそうな場合は、新しくActivityを作成して切り出した。
そうすることで、特定のAcitivityだけで管理され、局所化した。
インターフェース地獄
だが、この解決方法だとJavaのインターフェース地獄になるのは間違いない。
とくに、Fragmentが様々なActivityに表示される場合、ActivityとFragmentのペア事情が分からず、 改修初期はテンプレートで生成された通りのRuntime Exceptionのログを見ることになる。
一つ一つぶち当たって対処した。
タブレット用レイアウト
Kii Cloud の RxJava 2 ライブラリ作った
RxKii
RxKii という Kii Cloud の RxJava 2 ラッパーライブラリを作りました。
RxJava 2 の Completable or Single or Observable を返してきます。
クラス名は、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 チェックを行っています。
ここは @Nullable の間違いだろうと Kii のTwitterに報告済みです。
Completable を flatMap で繋ぐ際の注意点
また、 README に書いたサンプルを実行した際に気付きましたが、Completable から Single に繋ぐ際に、 Maybe を経由し、 flatMapSingle すると NoSuchElementException になるので、ご注意。
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