Edited at

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

More than 1 year has passed since last update.

データバインディングで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は使えました。