安卓的DataBinding

背景

在MVVM架构中,常用的做法是将View层与ViewModel层的数据进行双向绑定,
但目前比较容易搜索到的是单向绑定,
即使用LiveData的observe方法,
将ViewModel的变动通知给View层.

基于xml的双向绑定方案

是谷歌提供的数据绑定方法

需要先在gradle中配置使用databinding
当然ViewModel相关的lifecycle库也需要引入

1
2
3
4
5
android {
dataBinding {
enabled = true
}
}

ViewModel里不需要特别写什么

1
2
3
4
class MainActivityViewModel : ViewModel() {
private var _et: MutableLiveData<String> = MutableLiveData()
val et = _et
}

在xml布局中需要使用更多一层嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="utf-8"?>

<!-- 现在是最外层 -->
<!-- 原本的android,app,tools定义挪到了这里 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
<variable
<!-- 相当于rename -->
name="viewmodel"
<!-- 需要指定ViewModel的定义 -->
type="com.example.testdatabinding.MainActivityViewModel" />
</data>

<!-- 原本是最外层 -->
<androidx.constraintlayout.widget.ConstraintLayout
<!-- ... -->
tools:context=".MainActivity">

<EditText
android:id="@+id/input"
<!-- 主动的地方要用等于号 -->
android:text="@={viewmodel.et}"
<!-- ... --> />

<TextView
android:id="@+id/display"
<!-- 被动的地方直接复制 -->
android:text="@{viewmodel.et}"
<!-- ... --> />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

最后在activity中引入使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import <package>.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)

// 这里替换原先的指定layout的方法
// setContentView(R.layout.activity_main)
// 此处的类型不可缺
// 但这个类型被编译成了java,放在databinding路径下,名称几乎无法预测
val binding : ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

// 定义viewmodel可以完成viewmodel到view的映射
binding.viewmodel = viewModel

// 定义lifecycle可以完成view到viewmodel的映射
binding.lifecycleOwner = this
}
}

即可实现双向的绑定

基于anko的双向绑定方案

databinding可能也是实现了各种onChangeListener才能够正常通信的,
anko无法提供 layout_id 因此想自行实现两个方向的做法.

两个单向绑定

  1. ViewModel通知View

    使用LiveData的observe方法

    1
    2
    3
    mMainActivityViewModel.displayStr.observe(this, Observer<String> {
    show.text = it
    })
  2. View层通知ViewModel层

    可能需要手动在onChangeListener中写viewModel的postvalue语句来更新数据,

    1
    2
    3
    4
    5
    textChangedListener {
    afterTextChanged {
    viewModel.et.postValue(it.toString())
    }
    }

    然后将这个手动操作的内容封装进扩展函数以减少代码量.

双向绑定

双向绑定时需要考虑对于TextView和EditText防止死循环上的不对称性
EditText通常先于viewModel变化,
TextView通常落后与viewModel变化.

想要执行的等效版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
verticalLayout {
uiet = editText() {
textChangedListener {
afterTextChanged {
// editText不用判断是否需要更新viewmodel
viewModel.et.postValue(it.toString())
}
}
}

uitv = textView() {
textChangedListener {
afterTextChanged {
if (viewModel.et.value != it.toString()) {
// 需要防止textView值被viewModel改变后再次改变viewModel
viewModel.et.postValue(it.toString())
}
}
}
}

viewModel.et.observe(this@MainActivity, Observer {
// textView不用判断是否需要更新外观
uitv.text = it
if (uiet.text.toString() != it) {
// 需要防止editText更改viewModel值后再被viewModel更改
uiet.setText(it)
}
})
}

写到扩展函数中就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
        verticalLayout {
editText {
// 可以看出使用了扩展函数,view的定义少了很多
bindText(viewModel.et, this@xxxActivity)
}

uitv = textView {
bindText(viewModel.et, this@xxxActivity)
}

}

// 幸亏需要扩展的形式不多,不然扩展函数太大篇幅了
// lifecycleOwner不好在扩展函数中获得,因此直接使用参数传递
fun TextView.bindText(field: MutableLiveData<String>, lo: LifecycleOwner) {
// bind viewModel to view
field.observe(lo, Observer {
if (text.toString() != it) {
// 无论是TextView还是EditText,都在更新外观前判断是否需要更新外观
text = it
}
})

// bind view to viewModel
textChangedListener {
afterTextChanged {
if (field.value != it.toString()) {
// 无论是TextView还是EditText,都在更新viewModel前判断viewModel是否需要更新
field.postValue(it.toString())
}
}
}
}

欠缺思考

  • [ ] 写死了数据的类型为 String,想写成泛型
  • [ ] 写死了更新动作为 setText,想通过参数指定,setText对应的是String,所以需要在真正用到更多类型的时候再考虑
  • [ ] 不能在扩展函数中直接获得LifecycleOwner,总是需要参数传递…
    • 在anko layout 组件下直接使用 context 获得的并非activity而是 androidx.appcompat.view.ContextThemeWrapper

参考

  1. 基于xml的数据绑定方法
  2. 使用了泛型的例子