Help us understand the problem. What is going on with this article?

Androidの双方向(2-way)DataBinding

More than 1 year has passed since last update.

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を正しく設定すれば、このように定義したsetColorListenerInverseBindingListenerが渡されてくるので、プロパティ変更時にInverseBindingListeneronChange()を呼び出すようにします。
ColoPickercolorプロパティを例に説明しましたが、このメソッドの定義を一般化すると次のような感じなはずです。

@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、サンプルコードなどがより本格的にサポートされて流行っていくといいなーと思いました。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away