RxJava と Android の DataBinding と MVVM についての話をします。
Everything is a stream
via Rob Wormald
いいコトバですね。
Android アプリ開発では、事実上 stream is an (RxJava の) Observable<T>
、
つまり
Everything is an Observable<T>
と言ってもいいでしょう。
ところで、 Android アプリ開発でも DataBinding の登場以来、MVVM パターンの話題が増えてきました。
Everything is a Observable<T>
、では ViewModel はどうでしょう?
public class MainViewModel {
public final ObservableField<String> name = new ObservableField<>();
public final ObservableField<String> location = new ObservableField<>();
public final ObservableField<String> bio = new ObservableField<>();
}
ObservableField<T>
… Observable<T>
じゃない!(BaseObservable
でも同じね)
これの何がイヤかというと、Model から流れてくる値を View に表示してやるために、ViewModel で subscribe しないといけない所です。
public class MainViewModel {
public MainViewModel() {
// めんどい
_someModel.getUserName().subscribe(x -> {
name = x;
});
}
}
RxJava の Observable<T>
をそのまま View にバインドできたらどれだけ楽だろうか、それを実現してくれるのが rx-property-android というライブラリです。
- k-kagurazaka/rx-property-android: Bindable and observable property for Android Data Binding
- RxPropertyでRxJavaとAndroid Data Bindingを連携する - Qiita ←作者: @k-kagurazaka@github さんによる解説
rx-property-android を使ってみる
「UserID を指定して、GitHub のユーザー情報を取得する」という単純なサンプルを作ってみます。
こんなのです。
実装の説明の前に、MVVM に関するこのスライドを見てください。
- Vue.jsで実現するMVVMパターン Fluxアーキテクチャとの距離 // Speaker Deck
- 実況中継シリーズ Vue.jsで実現するMVVMパターン Fluxアーキテクチャとの距離 - Re.Ra.Ku アドベントカレンダー day 13 - Re.Ra.Ku tech blog
とっても勉強になります。
- 「注意深く設計された MV は単一データフローになる」*
- 「ModelについてViewModelが行うことは、イベントに対する反応と戻り値のないメソッドの呼び出ししかない」 via MVVMのModelにまつわる誤解
では実装を説明します、まずモデル。
ロジック自体は本題ではないので、公開インターフェースだけ載せます。
// GutHubModel.java
public class GitHubModel {
// 外部に公開する Hot な Observable
public final Observable<User> user = _user;
// 指定した UserID の GitHub ユーザー情報を得る
// Model だから戻り値の無いメソッドだよ
public void getUser(String user) {
/* ごにょごにょやって user の onNext を呼び出す */
}
}
「イベントに対する反応」が Observable<User> user
で、
「戻り値のないメソッド」が void getUser(String user)
ですね。
メソッドが呼ばれると、GitHub API で通信して User を得、user
の onNext
を呼び出して、変更を通知します。
次、本題の ViewModel です。
// MainViewModel.java
public class MainViewModel {
// View(EditText) と TwoWay Binding する RxProperty
public final RxProperty<String> userId = new RxProperty<>();
// ボタンをおした時に実行されるコマンド(=ボタンを押した時に onNext が呼ばれる Observable)
public final RxCommand<Void> getUserCommand;
// Model -> View DataBinding 用の RxProperty(name.value で DataBinding できる)
// OneWay Binding だから ReadOnlyRxProperty の方が望ましい
public final RxProperty<String> name;
public final RxProperty<String> location;
public final RxProperty<String> bio;
// GitHub からユーザー情報を得る Model. DIしようね
final GitHubModel _gitHubModel = new GitHubModel();
public MainViewModel() {
// Model -> ViewModel -> View のデータの流れ道を作っておきましょう
// GitHubModel の user が変更されたら、name/location/bio にそれぞれデータを流すよ
name = new RxProperty<>(_gitHubModel.user.map(user -> user.getName()));
location = new RxProperty<>(_gitHubModel.user.map(user -> user.getLocation()));
bio = new RxProperty<>(_gitHubModel.user.map(user -> user.getBio()));
// ユーザー取得コマンドは userID が空でない時のみ使用可能です
getUserCommand = new RxCommand<>(userId.asObservable().map(x -> !TextUtils.isEmpty(x)));
// ユーザー取得が実行されたら GitHubModel の getUser を呼ぶよ
getUserCommand.asObservable().subscribe(x -> {
_gitHubModel.getUser(userId.get());
});
}
}
rx-property-android を使用した結果、
-
ObservableField<T>
がRxProperty<T>
に -
ボタンのイベントハンドラ
がRxCommand
に
置き換わっています。
そしてこれらは、その名が示すとおり全て Observable<T>
と連携しています。
例えば RxProperty<String> name
は、 _githubModel.user
を map オペレータで getName()
に変換しています。他の2つも同様です。
RxCommand getUserCommand
も Observable<T>
です。
これは、「コマンドが実行されたら onNext() が呼び出される」という Observable です(RxBinding のようなイメージ)。
このように、全て Observable<T>
にすることができました。
イミュータブルなフィールドとコンストラクタで「定義」するだけ、美しい・・・。
Everything is an Observable<T>
, ViewModel is also Observable<T>
さて、ではこの RxProperty<T>
と RxCommand
を View(xml) でバインドします。
// activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="net.amay077.rxretrofitsample.MainActivity">
<data>
<variable name="viewModel" type="net.amay077.rxretrofitsample.viewmodels.MainViewModel"/>
</data>
(中略)
<EditText android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={viewModel.userId.value}"/>
<Button
android:id="@+id/buttonGetUser"
android:text="Get User"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:rxCommandOnClick="@{viewModel.getUserCommand}"
/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.name.value}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.location.value}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.bio.value}"/>
(中略)
</layout>
RxProperty のバインドは <プロパティ名>.value
とします。 .value をよく忘れるので注意が必要です。
RxCommand は app:rxCommandOnClick
を使ってバインドします。
ソースコード
完成形のソースコードは、
にあります。
まとめ
rx-property-android を使うと DataBinding と Observable<T>
の連携ができます。
全てが Observable<T>
なら、
「左手はそえるだけ、ViewModel はつなぐだけ」
流れるような DataBinding + MVVM ライフを。
おまけ
rx-property-android は、 .NET の ReactiveProperty というライブラリを Android 向けに移植してくださっているものです。
ReactiveProperty は .NETアプリケーション他、 Xamarin によるモバイルアプリケーションでも多数の事例があります(NHK紅白アプリとかね)。
私は DroidKaigi 2017 で Xamarin.Android について話しますが、ReactiveProperty を使った Android/iOS 共通な ViewModel の話も少ししたいなーと思っています、ご興味あれば是非。