データバインディングでLiveDataが使えるようになったみたいな話があったので、いじってみたときのメモ。
ユーザ名とパスワード、規約にチェックしてないとsubmitボタンを押せなくするみたいな制約条件をデータバインディングで実現するのが今回のテーマです。
レイアウト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なバインディングをやろうとすると、おそらくこんな感じになるはず。
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とか使って
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になると・・・
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() }
}
}
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ベースで使用することもできるようになり、画面回転がかかわるユースケースでも割と平気になった。
databinding v2と compiler 3.1.0
よくわかってなくて、@star_zero さんからコメントで指摘をいただきました。(ありがとうございました!
v2だからLiveData対応したというわけではなくて、v1のままでも apply plugin: 'kotlin-kapt'
すればLiveDataは使えました。