KotlinConf 2018 - Shaping Your App's Architecture with Kotlin and Architecture Components
KotlinConf 2018のビデオでAndroidアプリの設計について面白いものがあったのでざっくりですがまとめてみました。
一昔前の設計のアプリをArchitecture ComponentsやCoroutinesを用いて再設計している実際の経験に基づいた話でとても参考になりました。
全体設計
UI, Domain, Dataのレイヤードアーキテクチャ。これは最近まあよく見るので特に目新しさはないですね。
- Data層:Repository, DataSource (Local/Remote)
- Domain層:UseCase
- UI層:ViewModel, Activity/View, XML
レイヤー間はCoroutinesでやり取りしてます。学習コストや利用の容易さからRxJavaではなくCoroutineを採択したようです。
Data層
Data層を構成するのは以下の3要素。
- Remote DataSource:(HTTP)リクエストを構築してサーバからデータを取得
- Local DataSource:ディスクにデータを保存
- Repository:上記2種類のDataSourceを利用してデータの取得と保存; 任意でインメモリにデータをキャッシュ
インメモリキャッシュは専用のシングルトンクラスとか作ったりすることが多かったんですが、Repositoryでインメモリキャッシュを持つという設計は目から鱗でした。こうすることでレイヤードアーキテクチャの他の層からインメモリキャッシュを参照するようなコードも防止できるし理にかなってる気がします。ただRepositoryのプロパティに単純に持つとしたらRepositoryのライフサイクルがどうなってるのか気になります。
また設計にはあまり関係ないですが、生成されるオブジェクトの数を減らす(?)テクニックとしてinline classが紹介されていました。 下の写真の例では、1つのRepositoryに第2引数だけが異なるpostCommentという同名メソッドを定義する方法として、第2引数のLong型の変数をそれぞれ別のinline classにしています。 つまりプリミティブ型の変数に名前をつけて区別するようなものでしょうか(知らんけど)。
Domain層
UseCaseの責務は
- ビジネスロジックに基づいてデータを処理する
- たった1つのタスクを持つ
- チームのルールとして1つのUseCaseには1つだけのpublicメソッド、他は全てprivateメソッド
- その1つのpublicメソッドはinvokeメソッドにして関数オブジェクトとして扱う
- ユーザのログイン状況の確認などはUseCaseの責務(UseCaseがLoginRepositoryを扱う)
UI層
ViewModelの責務は
- UIで表示されるデータを公開(LiveData)
- ユーザアクションに基づいてUseCaseのアクションを実行
- Coroutinesの開始とキャンセル
ViewModelはActivityとXMLの2箇所から参照される。 ViewModelとXMLの間のデータのやり取りはデータバインディングを用いる。
- ViewModelは関連するいくつかのUseCaseや他の引数をコンストラクタで受け取ってimmutablityを保つ
- ViewModelProvidersで生成するViewModelはデフォルトでは引数を受け取れないため、Factoryを拡張する
- 全てのViewModelには対応するFactoryを作成する
動画では、ViewModel自身とstoryIDのimmutabilityを保つために同じActivityクラスが複数インスタンス存在するとしても、それぞれ別インスタンスのViewModelを持つ、というニュアンスのことを言ってる?ような気がするんだけど、これはViewModelのそもそもの目的からちょっと外れる気がするのでどういうことかよく理解できない。分かる人教えて欲しい。
- Activityからの編集を防止するためにViewModelではMutableLiveDataをprivateにしてImutableなLiveDataを宣言して公開する
- ImutableなLiveDataのgetterはMutableLiveDataをそのまま返却する
- URLを開いたりトーストの表示などは、独自のEvent型のLiveDataで扱う
- Event型を利用することで、再度observeされた時に重複してイベントが発火しないようにする
- Event型の詳細については以下の別ブログ参照
まとめ
- よく見るレイヤードアーキテクチャ+Coroutinesを採択
- インメモリキャッシュはRepositoryの責務
- UseCaseは最小単位で実装して関数オブジェクトとして扱うといい感じ
- ViewModelは独自のFactoryクラスを実装してUseCaseやその他データをコンストラクタで受け取ってimmutableに保つ
- 一度きりのUIイベント等をViewModelで管理する手段として独自のEvent型のLiveDataを利用する
こんなところでしょうか。 動画の中では他にも、Data層のRepositoryがKotlinで書かれたDomain層からだけじゃなく古いJavaコードやUI層のコードからも参照される状況の対応策や、複雑なデータ構造のデータを生成するための拡張実装についてなど、有益そうな情報がたくさんありました。