shibuya.apk #9 (2016/07/15)で双方向DataBindingについて発表した内容を書きます。発表に使ったスライド
おさらい
普通のDataBinding
<layout>
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout …>
<TextView android:text="@{user.name}" …/>
</LinearLayout>
</layout>
public class User {
public final ObservableField<String> name =
new ObservableField<String>();
}
この例では、UserクラスのnameというフィールドをTextViewにバインドしています。これにより、User.setName("tarou")
を呼び出した時に自動でTextViewのテキストも変更されます。
上記のように、バインドするフィールドをObservableField<>
として定義する方法と、
private String name;
@Bindable
public void getName() { return name; }
public void setName(String name){
this.name = name;
notifyPropertyChanged(BR.name);
}
こんな感じでgetterもしくはプロパティ定義に@Bindable
アノテーションをつけ、setterでnotifyPropertyChanged(BR.プロパティ名)
を呼ぶ方法があります。
MVVM
Model-View-ViewModelの略です。
ViewModelとは、*Viewを描画するための状態の保持と、Viewから受け取った入力を適切な形に変換してModelに伝達する役目を持つ(Wikipedia)*ものです。
ViewModelにUIロジックなどを実装し、ViewとViewModelをDataBindingでつなぐことでViewを表示します。
ViewModelはModelの影と言われます。
双方向(2-way)DataBinding
双方向DataBinding自体は新しい概念ではなく、Angular.jsなどでも有名なものです。
普通のDataBindingとの比較
- 普通のDataBinding:
- ViewModelの値を変更する -> Viewに自動で反映されてる!
- 双方向DataBinding:
- ViewModelの値を変更する -> Viewに自動で反映されてる!
- ユーザがViewに入力 -> 自動でViewModelに値がセットされてる!
というようなものです。「自動」と書きましたが、内部的にはViewModelのプロパティと対応するViewのマップのようなものを保持して、ViewModelのプロパティをObserveしておき、更新があれば対応するViewに値を渡して見た目を変更するということをやっています。双方向の場合はViewからの入力も監視し、入力があった時に入力値をViewModelの対応するプロパティのsetterに渡します。
Androidの双方向DataBinding
2016/07/15の時点でAndroid公式ドキュメントのDataBindingのガイドに特に記述がないですが、Googleの中の人がブログに書いていました。
2-way Data Binding on Android! | halfthought
AndroidStudio2.1~ , gradle2.1.0から使えます。ベータ版なのかドキュメントがまだできてないだけなのかわかりませんが、仕様が変わる可能性もあるかもしれません。
@={vm.name}
通常のDataBindingは@{vm.name}
という書き方をしますが、双方向では@={vm.name}
のように@の後ろに=をつけます。
例:EditText(TextView)
例えばアカウント登録のような画面で「名前が入力されている時は送信ボタンを有効にし、入力がない時は送信ボタンを無効にする」という機能を実装するのは次のようなコードで書けます。Java側は通常のDataBindingと同じようなコードでよく、双方向特有の記述などは要りません。
<variable type="com.example.app.SignUpViewModel" name="viewModel"/>
<EditText android:text="@={viewModel.name}" …/>
<Button android:enabled="@{viewModel.btnEnabled}" …/>
public class SignUpViewModel extends BaseObservable {
@Bindable
private String name;
public boolean getBtnEnabled() {
return !TextUtils.isEmpty(name);
}
public String getName() { return name; }
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
notifyPropertyChanged(BR.btnEnabled);
}
…
}
双方向DataBindingにより、ユーザがEditTextに何かしら入力した時にSignUpViewModel.setName(String name)
に入力値が渡されてきます。
例:RadioGroup
EditTextのandroid:text
だけでなく、様々なViewのプロパティに対応してます。これはRadioGroupの例です。
<RadioGroup android:checkedButton="@={viewModel.gender}" …>
<RadioButton android:id="@+id/male" …/>
<RadioButton android:id="@+id/female" …/>
</RadioGroup>
public class SignUpViewModel{
public final ObservableInt gender = new ObservableInt();
...
}
@Bindable
アノテーションによるプロパティだけでなく、ObservableInt
などのdatabinding用のフィールドにももちろん双方向Bindingできます。
対応してるもの
他には、次のようなものに対応してます。
- AbsListView
android:selectedItemPosition
- CalendarView
android:date
- CompoundButton
android:checked
- DatePicker
android:year, android:month, android:day
- NumberPicker
android:value
- RatingBar
android:rating
- SeekBar
android:progress
- TabHost
android:currentTab
- TimePicker
android:hour, android:minute
なんでこれだけなんだろうという点ですが、例えばAbsListViewには選択されてる要素の変更の通知を受けるためにOnItemSelectedListener
のセットができます。このようにプロパティの変更通知リスナーがセットできるものに関しては双方向DataBindingの対応をしてるが、そうでないものは実装が難しいので対応してないということだそうです。
自作View
自作Viewにも双方向DataBindingの実装ができます。
例えばユーザが好きな色を選択できるColorPicker
というViewを作ったとし、選択された色を示すcolor
プロパティを双方向Bindingするには下記のようなコードになります。
<ColorPicker app:color="@={viewModel.myColor}" …/>
@InverseBindingMethods({
@InverseBindingMethod(type = ColorPicker.class, attribute = "color"),
})
public class ColorPicker extends View {
public void setColor(int color) { /* ... */ }
public int getColor() { /* ... */ }
public interface OnColorChangeListener {
void onColorChange(ColorPicker view, int color);
}
@BindingAdapter("colorAttrChanged")
public static void setColorListener(ColorPicker view,
final InverseBindingListener listener) {
if (listener == null) {
view.setOnColorChangeListener(null);
} else {
view.setOnColorChangeListener((view1, color) -> listener.onChange());
}
}
}
双方向Bindingのために追加で必要なコードは次の2箇所です。
@InverseBindingMethods({
@InverseBindingMethod(type = ColorPicker.class, attribute = "color"),
})
ここでは@InverseBindingMethods
アノテーションで双方向Binding対応するクラスやプロパティを設定しています。
@BindingAdapter("colorAttrChanged")
public static void setColorListener(ColorPicker view,
final InverseBindingListener lister) {
if (colorChange == null) {
view.setOnColorChangeListener(null);
} else {
view.setOnColorChangeListener((view1, color) -> lister.onChange());
}
}
@InverseBindingMethods
を正しく設定すれば、このように定義したsetColorListener
にInverseBindingListener
が渡されてくるので、プロパティ変更時にInverseBindingListener
のonChange()
を呼び出すようにします。
ColoPicker
のcolor
プロパティを例に説明しましたが、このメソッドの定義を一般化すると次のような感じなはずです。
@BindingAdapter("プロパティ名AttrChanged")
public static void 任意のメソッド名(対象のView view,final InverseBindingListener listener)
Inverse(反対の)というワードが出てきていますが、通常のDataBinding(ViewModelを変更->Viewも変更される)に対して、反対のBinding(Viewを変更->ViewModelも変更される)に関する処理ということでしょう。
テスト
双方向DataBindingに限ったことでなくDataBinding/MVVMの話ですが、ViewModelが特にAndroidフレームワークに依存していなければ次のように簡単にテストできます。
@RunWith(JUnit4.class)
public class SignUpViewModelTest {
private SignUpViewModel viewModel;
@Before
public void setUp() {
viewModel = new SignUpViewModel(null);
}
@Test
public void setNames_FullNameIsCorrect() {
viewModel.setFirstName("山田");//@BindableによるフィールドもOK
viewModel.lastName.set("太郎");//ObservableFieldもOK
assertEquals(viewModel.getFullName(), "山田太郎");
}
}
懸念点
これも双方向DataBindingに限ったことでなくDataBinding/MVVMの話ですが、不安な点やデメリットとして次のようなものがあると思いました。
- ダイアログを使った入力処理や、トースト表示のView操作の処理など、全てのUI処理に (双方向)DataBindingが適切なわけではないので、各々で処理の書き方がまちまちになり一貫性がなくわかりづらくなるかも。
- XMLに色々書きすぎてなんで動いてるかわかんない問題
- IDEの機能微妙になる(必要なクラスやフィールドがコンパイル時生成なので赤字まみれの中でコードを書かないといけなかったり、xml内での補完が効かないことがあったり、定義元ジャンプできなかったり)
まとめと感想
- 双方向DataBinding:
- ViewModelを変更したらViewに反映されてる。
- Viewに入力したらViewModelも変更されてる。
-
<EditText android:text="@={vm.name}" />
で双方向バインディングできる
DataBinding/MVVMに期待してるので、ドキュメント、IDE、サンプルコードなどがより本格的にサポートされて流行っていくといいなーと思いました。