2-wayデータバインディングが格段に実装しやすくなったAndroid data-binding 3.1.0

データバインディングでLiveDataが使えるようになったみたいな話があったので、いじってみたときのメモ。


Bitmap.png

ユーザ名とパスワード、規約にチェックしてないとsubmitボタンを押せなくするみたいな制約条件をデータバインディングで実現するのが今回のテーマです。

レイアウトXML →いままでどおり

res/layout/activity_signup.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>

        <variable
            name="viewModel"
            type="io.github.yusukeiwaki.twowaydatabindingpractice.SignupActivityiewModel"/>
    </data>

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="ユーザ名"
            android:layout_margin="8dp">

            <android.support.design.widget.TextInputEditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:singleLine="true"
                android:text="@={viewModel.username}"/>
        </android.support.design.widget.TextInputLayout>

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="パスワード"
            android:layout_margin="8dp">

            <android.support.design.widget.TextInputEditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:singleLine="true"
                android:text="@={viewModel.password}"/>
        </android.support.design.widget.TextInputLayout>

        <CheckBox
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:checked="@={viewModel.termOfUse}"
            android:layout_margin="8dp"
            android:text="利用規約に同意"/>

        <Button
            style="@style/Widget.AppCompat.Button.Colored"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:enabled="@{viewModel.canSubmit}"
            android:layout_margin="8dp"
            android:text="Signup"/>
    </LinearLayout>
</layout>

ビューモデル →もうBaseObservableとはおさらばできるぞ!

いままでのデータバインディングで2-wayなバインディングをやろうとすると、おそらくこんな感じになるはず。

SignupActivityiewModel
class SignupActivityiewModel : BaseObservable() {
    @Bindable
    var username: String? = null
        set(value) {
            field = value
            notifyPropertyChanged(BR.username)
            notifyPropertyChanged(BR.canSubmit)
        }

    @Bindable
    var password: String? = null
        set(value) {
            field = value
            notifyPropertyChanged(BR.password)
            notifyPropertyChanged(BR.canSubmit)
        }

    @Bindable
    var termOfUse: Boolean = false
        set(value) {
            field = value
            notifyPropertyChanged(BR.termOfUse)
            notifyPropertyChanged(BR.canSubmit)
        }

    private fun isValid() =
            !username.isNullOrBlank() && !password.isNullOrBlank() && termOfUse

    @get:Bindable
    val canSubmit: Boolean
        get() = isValid()
}

詳しくは以下の記事などが参考になる。

これ、実際に実装してみるとわかるのだけど、

  • BaseObservable継承しないといけないので、(アーキテクチャコンポーネントの)ViewModelは継承できない!
    • 画面回転時にViewModelが再...(以下略
  • @Bindable とか notifyPropertyChanged(BR.**) とか特殊な実装をいろいろやらないといけない
    • テスト書きづらい(というか書けない...)

などなど、わりと面倒なところもあって、実装コストvs効果を考えると正直あまり積極的に採用したいとは思えなかった。

完全に余談だけども、RxBindingとか使って

SignupActivity
override fun onCreate(...) {
  super.onCreate(...)
  binding = DataBindingUtil.setContentView(...

  // region: フォームバリデーション
  val usernameObservable = RxTextView.textChanges(binding.username)
  val passwordObservable = RxTextView.textChanges(binding.password)
  val termOfUseObservable = RxCompoundButton.checkedChanges(binding.termOfUse)

  val validationObservable = Observable.combineLatest(usernameObservable, passwordObservable, termOfUseObservable) { username, password, termOfUse ->
    !username.isNullOrBlank() && !password.isNullOfBlank() && termOfUse
  }
  validationObservable
      .compose(bindToLifecycle())
      .subscribe(RxView.enabled(binding.submitButton))
  // endregion
}

みたいに書いたほうがよっぽどスッキリ2-wayバインディングが書けるよね! ってのが本音だった。

 

 

これが、data-binding 3.1.0になると・・・

SignupActivityiewModel
class SignupActivityiewModel : ViewModel() {
    val username = MutableLiveData<String>()
    val password = MutableLiveData<String>()
    val termOfUse = MutableLiveData<Boolean>()

    private fun isValid() =
            !username.value.isNullOrBlank() && !password.value.isNullOrBlank() && termOfUse.value == true

    val canSubmit = MediatorLiveData<Boolean>().also { result ->
        result.addSource(username) { result.value = isValid() }
        result.addSource(password) { result.value = isValid() }
        result.addSource(termOfUse) { result.value = isValid() }
    }
}
SignupActivity
class SignupActivity : AppCompatActivity() {

    private lateinit var binding: ActivitySignupBinding
    private lateinit var viewModel: SignupActivityiewModel

    override fun onCreate(...) {
        super.onCreate(...)
        binding = DataBindingUtil.setContentView(...

        // region: setup ViewModel
        viewModel = ViewModelProviders.of(this).get(SignupActivityiewModel::class.java)
        binding.setLifecycleOwner(this)
        binding.setViewModel(viewModel)
        // endregion
    }
}

LiveDataをバインディングできるようになったおかげで、データの流れがMediatorLiveDataで大変わかりやすく書けるようになった。
あと、アーキテクチャコンポーネントのViewModelベースで使用することもできるようになり、画面回転がかかわるユースケースでも割と平気になった。

image

 

databinding v2と compiler 3.1.0

よくわかってなくて、@star_zero さんからコメントで指摘をいただきました。(ありがとうございました!

v2だからLiveData対応したというわけではなくて、v1のままでも apply plugin: 'kotlin-kapt' すればLiveDataは使えました。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.