Experiments Never Fail

Android Data Binding + MVVMパターンのサンプルを書いてみた

notifyPropertyChanged とか、どこかで見たことのある機能が満載の Android Data Binding ですが、登場以来あまり追えてなかったのでやっとサンプルをつくってみました。

といっても

で作ったストップウォッチアプリを Data Binding 化しただけです。

前回 との違いを図に示します。

MainActivity のバインディングの定義 #

activity_main.xml はこんな感じ。

@{ }MainViewModel に用意した ObservableField<T> または、イベントハンドラとバインドしてます。

<!--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">


<data>
<variable name="viewModel"
type="com.amay077.stopwatchapp.viewmodel.MainViewModel"/>

</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:orientation="vertical"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">


<TextView android:id="@+id/textTime"
tools:text="00:00.000"
android:text="@{viewModel.formattedTime}"
android:textSize="50sp"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content" />


<Button
android:id="@+id/buttonStartStop"
android:text="@{viewModel.runButtonTitle}"
android:onClick="@{viewModel.onClickStartOrStop}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<Button
android:id="@+id/buttonLap"
android:text="Lap"
android:enabled="@{viewModel.isRunning}"
android:onClick="@{viewModel.onClickLap}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<Switch
android:id="@+id/switchVisibleMillis"
android:checked="@{viewModel.isVisibleMillis}"
android:onClick="@{viewModel.onClickToggleVisibleMillis}"
android:text="小数点以下を表示"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />


<ListView
android:id="@+id/listLaps"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:formattedLaps="@{viewModel}" />

</LinearLayout>
</layout>

ListView で app:formattedLaps="@{viewModel}" としているところだけが特殊で、これは MainActivity.java に定義したカスタムSetter を呼び出します。

MainActivity.java はこんな感じ。

//MainActivity.java
public class MainActivity extends AppCompatActivity {

private /* final */ MainViewModel _viewModel;
private CompositeSubscription _subscriptions = new CompositeSubscription();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

_viewModel = new MainViewModel(this.getApplicationContext());
binding.setViewModel(_viewModel);

// ■ViewModel からの Message の受信(省略)
}

/**
* ListView と ViewModel のカスタムバインディング
*
* TODO 本当は viewModel.formattedLaps とバインドしたい
*/

@BindingAdapter("formattedLaps")
public static void setFormattedLaps(ListView listView, final MainViewModel viewModel) {
final LapAdapter adapter = new LapAdapter(listView.getContext());
listView.setAdapter(adapter);

// formattedLaps が変化した時に呼ばれるイベントで、Adapterを洗い替え。
viewModel.formattedLaps.addOnPropertyChangedCallback(new android.databinding.Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(android.databinding.Observable sender, int propertyId) {
adapter.clear();
adapter.addAll(viewModel.formattedLaps.get());
}
});

// バインド時に値を更新
adapter.clear();
adapter.addAll(viewModel.formattedLaps.get());
}

@Override
protected void onDestroy() {
_viewModel.unsubscribe();
super.onDestroy();
}
}

オレオレBindingがごっそり消えてスッキリ。
setFormattedLaps がカスタムSetterで、この中で MainViewModel.formatterLaps を監視し、値が変わったら Adapter を総入れ替えしてます。が、これが正しいやり方かわからない。
extensions/baseAdapters/src/main/java/android/databinding/adapters にはそれらしいのがないでござるよ。。。

ViewModel 側 #

この辺みてください。大したことはやってないです。(急に雑になったw)

ObservableUtil.toObservableField とか、もうどっかの誰かがやってそうだし、事実上標準の何かが出てきそうな気がすごくします。

おまけ #

Messenger を RxJava ベースにした #

らしいので、自作してた MessengerRxJava ベースにしてみました
ViewModel→Viewの通知
にしか使ってないので、あまり rx.Observable<T> にする旨味はなかったですね。あ、ofType って便利ですね。

まとめ #

今回作ったアプリの全ソースは

です。

.NETアプリケーション開発では、ViewModel を View にバインドすることが殆どなので、典型的な例としてやってみました。

レイアウトに直接バインドを定義できるので、コードビハインド(Javaのソース)はスッキリしますが、個人的にはあまり好きではありません。
コードビハインドに(textTime.SetBinding(v => v.Text, viewModel.Time) みたく)書いた方が、定義情報がまとまっていて管理しやすい、デバッグしやすいと思うからです。(同じ理由で、xmlに直接記述する Expression Language も好きではありません。)
が、今のところ、Android Data Binding では、レイアウトXMLでしかバインディングを定義できないようですね。

ともあれ、AndroidBinding とか Butter Knife はこれで駆逐されていく(前者はすでに息してなさそうですが)と思うので、新しいアプリ開発では積極的に使っていこうかなと思います。

参考 #

published at tags: Android MVVM DataBinding RxJava