data bindingちゃんと触ったことがなかったので、data bindingを使ったMVVMなアプリを書いてみました。
機能を限定したGitHubクライアントです。よくあるTODOアプリみたいなシンプルなものだと実戦投入したときの詰まりどころがよくわからない気がしたので、以下のような画面を含むそこそこ複雑なアプリにしてみました。
- リポジトリの一覧画面と詳細画面があり、それぞれでスターできる。各画面でのスター操作の結果は同期している。
- リストビューでサーバーから取得したリポジトリ一覧を表示する。一度取得したリストはSQLiteにキャッシュされ、次回以降はキャッシュから値を取得する
- ViewPagerで複数のFragmentを表示している。Fragment Aでの操作が、Fragment Bにも影響する。
アーキテクチャ
ビューとビューモデルの間はdata bindingによってバインドされます。
また、ビューモデルからビュー、ビューモデルから他のビューモデルへのイベント通知にはEventBusを使用しています。
モデル各層のインタフェースはRxJavaのObservableを返すようにしています。
View
それぞれのActivityやFragmentは対応するビューモデルを一つ持つようにしています。モデルの呼び出しはビューモデルに書くので、ビューでの処理は
- リストビューのセットアップ
- メニュー関連の処理
- アクションバー関連の処理
くらいになります。
また、レイアウトXMLには基本的に対応するビューモデルのインスタンスを一個だけ参照するようにしてます。
実装例
- Activity: https://github.com/kobakei/Anago/blob/master/app/src/main/java/io/github/kobakei/anago/activity/SignInActivity.java
- XML: https://github.com/kobakei/Anago/blob/master/app/src/main/res/layout/sign_in_activity.xml
ViewModel
各画面に対応するクラスで、画面に反映する変数を所持したり、ビューで発生したイベントを受け取ってモデルを呼び出したりします。モデルを呼び出した結果はRxJavaのObservableで返ってくるので、subscribeして諸々の処理を実行します。
ビューへの変数の反映はdata bindingがやってくれます。アクションバーなどdata bindingがカバーしてないビューの操作は、EventBusでビューにイベントを飛ばしてビューにやってもらっています。
実装例
Model
モデルはさらに3つのレイヤーに分かれます。
usecase
usecaseは、一連のビジネスロジックを表現するクラスです。
- サインインする
- リポジトリにスターをつける
などの単位で一つのクラスにします。複数のビューモデルから呼び出されることもあります。
実装例
repository
repositoryは、データのCRUD操作を提供するクラスです。エンティティごとに対応するリポジトリを用意します。内部ではサーバーからデータを取得したり、SQLiteから取得したりしますが外側には隠蔽します。
この層以下はアプリケーションと同じライフサイクルを持つので、Singletonなクラスになります。
実装例
data
dataは、RESTful APIやSQLiteから取得した生の値を、エンティティに変換するクラスです。RetrofitやOrmaなどのライブラリを使うことがほとんどです。
サンプルでは、netとdaoというパッケージが該当します。
使用した主なライブラリ
Dagger 2
依存性注入用ライブラリ。上の図にあるように、各層は一つ下の層のインスタンスに依存しているので、これをDaggerで自動的に注入してます。
RxJava
※はじめてがっつり触ったのでやや理解が怪しいので注意
RxJavaを使うメリットは、コールバックのネストを防げるのと、RxLifecycleを一緒に使うことでgetActivity()==null問題を回避できることかと思います。通信など時間のかかる処理をしてる間にユーザーがバックキーを押してActivityが破棄されるようなケースに、いちいちgetActivity()のnullチェックをしなくても自動的にストリームの処理が終わるので扱いやすいです。
なお、RxJavaは使わないでアーキテクチャを実現することもできますが、その場合はgetActivity()==null問題対策のためにEventBusを駆使してビューモデルからビューにイベントを通知してあげたりしないといけません。
Retrofit, Android-Orma
データ層のRESTful APIとSQLite用の実装に使用してます。この2つはRxJavaのObservableを返すインタフェースを提供しているので、アプリ全体でObservableを渡していくような場合に相性がよいです。
EventBus
RxJavaを非同期処理に使う場合でも、ビューモデルからビューモデルにイベントを飛ばす場合などはEventBusが欲しくなるので使ってます。
また、ActionBarなどdata bindingの対象にないUIをビューモデルでの処理後に操作したい場合は、EventBusでビューに通知してます。
その他メモ
ビューモデルの持つリストにアイテムが追加/削除されたときに、自動でListView/RecyclerViewを更新する
- アダプターのコンストラクタ、ObservableArrayListを渡す
- コンストラクタ内で、
ObservableArrayList#addOnListChangedCallback
を呼んでnotifyDataSetChanged();
を呼ぶ
ActionBarなどdata binding外のUI操作
ビューモデルからビューにEventBusでイベントを飛ばして、ビューで操作してます。
UIのライフサイクルに依存しない長い処理
IntentServiceを作り、その中でユースケースを呼び出してます。
リトライ処理やディスク永続化などの機能が欲しい場合は、job queueを使ったほうがよいかもしれないです。
ビューモデルのテスト
そもそもあまりテストかけてないが、ビューモデル単体でのテストをする場合は処理を実行してObservableFieldが期待した値になっているかを確認するような感じになるのかなーと。
所感
- ビュー操作周りのコード(setTextとかsetVisibilityとか)が減ったので、ビュー&ビューモデルの見通しはよくなった気がします。
- data bindingの使い方は公式ドキュメントが割とよくまとまってます
- ただし、EditTextから入力値をデータバインディングでビューモデルに持ってくる方法は書いてなかった気がする
android:text="@={viewModel.name}"
- http://blog.techium.jp/entry/2016/05/19/014241
- data bindingのイベントハンドラは主なものはすでに定義されているが、定義されている属性リストが見やすい形でまとまってないのでちょっとつらい。。
- ただし、EditTextから入力値をデータバインディングでビューモデルに持ってくる方法は書いてなかった気がする
- モデルの分割粒度は、アプリの規模や内容次第です。例えばサーバーからデータを取ってそのまま表示するようなアプリだと、リポジトリもユースケースもObservableをそのまま上にわたすだけの実装になるので、あまり分割する必要性はないかもです
参考にした資料
- data bindingまわり
- 公式ドキュメント: https://developer.android.com/topic/libraries/data-binding/index.html
- 解説: http://qiita.com/izumin5210/items/2784576d86ce6b9b51e6
- 解説: https://tech.recruit-mp.co.jp/mobile/android-data-binding/
- 定義済み属性: https://android.googlesource.com/platform/frameworks/data-binding/+/android-7.0.0_r5/extensions/baseAdapters/src/main/java/android/databinding/adapters
- 双方向バインディング: http://blog.techium.jp/entry/2016/05/19/014241
- アーキテクチャまわり