Kotlin 实战 | 构建布局 DSL 中的数据绑定

上一篇介绍了运用 Kotlin DSL 构建布局的方法,相较于 XML,可读性和性能都有显著提升。如果这套 DSL 还能数据绑定就更好了,这一篇就介绍一种实现思路。

数据绑定原始方式

在没有 Data Binding 之前,我们是这样为控件绑定数据的:

class MainActivity : AppCompatActivity() {
    private var tvName: TextView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tvName = findViewById<TextView>(R.id.tvName)
    }
    
    override fun onUserReturn(user: User){
        tvName?.text  = user.name
    }
}
复制代码

tvName 被静态地声明在 XML 中,程序动态地通过 findViewById() 获取引用,在数据返回的地方调用设值 API。

“静态的”意味着可以预先定义,且保持不变。而“动态的”恰恰相反。

对于某个特定的业务场景,除了界面布局是“静态的”之外,布局中某个控件和哪个数据绑定也是“静态的”。这种绑定关系最初是通过“动态”代码实现的。直到出现了 Data Binding

Data Binding

它是 Google 推出的一种将数据和控件相关联的方法。

如果把 XML 称为 声明型的 ,那 Kotlin 代码就是 程序型的 ,前者是静态的,后者是动态的。为了让它俩关联,Data Binding 的思路是把 程序型的 变量引入到 声明型的 布局中,比如下面把 User.name 绑定到 TextView 上( data_binding_activity.xml ):

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.test.User"/>
   </data>

    <TextView 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@{user.name}"/>
</layout>
复制代码

其中 User 是程序型的实体类:

package com.test

data class User(var name: ObservableField<String>, var age: ObservableField<Int>)
复制代码

ObservableField 用于将任何类型包装成可被观察的对象,当对象值发生变化时,观察者就会被通知。在 Data Binding 中,控件是观察者。

在 Activity 中,这样写代码就完成了数据绑定:

class MainActivity: AppCompatActivity() {
    //'声明在 Activity 中的数据源'
    private var user:User? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //'为 Activity 设置布局并绑定控件'
        val binding = DataBindingUtil.setContentView<DataBindingActivityBinding>(this, R.layout.data_binding_activity);
        //'绑定数据源'
        binding.user = this.user
    }

    override fun onUserReturn(user: User){
        //'修改数据源'
        this.user.name.set( user.name )
    }
}
复制代码

这样写的好处是,Activity 中不会再出现 findViewById() 和各种为控件设置属性的方法,而只需要观察数据源的变动。

为 DSL 添加数据绑定

回顾下上一篇构建布局的 DSL :

class MainActivity : AppCompatActivity() {

    private val rootView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_id = "tvName"
                layout_width = wrap_content
                layout_height = wrap_content
                textSize = 30f
                textStyle = bold
                align_vertical_to = parent_id
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(rootView)
    }
}
复制代码

Activity content view 的根布局是 ConstraintLayout,其中包含一个 TextView。为了让它的值和 User.name 联动,新增的扩展方法如下:

inline var TextView.bindText: LiveData<CharSequence>?
    get() {
        return null
    }
    set(value) {
        //'为 TextView 的 text 属性绑定数据源'
        observe(value) { text = it }
    }

//'为控件绑定 LiveData 类型的数据源'
fun <T> View.observe(liveData: LiveData<T>?, action: (T) -> Unit) {
    (context as? LifecycleOwner)?.let { owner ->
        liveData?.observe(owner, Observer { action(it) })
    }
}
复制代码

然后就可以像这样为 TextView 绑定数据源了:

class MainActivity : AppCompatActivity() {

    private val nameLiveData = MutableLiveData<CharSequence>()

    private val rootView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_id = "tvName"
                layout_width = wrap_content
                layout_height = wrap_content
                textSize = 30f
                textStyle = bold
                //'绑定数据源'
                bindText = nameLiveData
                align_vertical_to = parent_id
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(rootView)
    }
    
    override fun onUserReturn(user: User){
        //'数据源变更'
        nameLiveData.value = user.name
    }
}
复制代码

布局 DSL 和界面类定义在同一个 kt 文件中,所以它能方便地访问到各种数据源。

把数据源都抽象为 LiveData<T> , 控件中每一个需要绑定数据的属性,都可以为其扩展一个 bindXXX 属性,它的值是 LiveData<T>

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章