Jetpack DataBinding

作者: 科技猿人 | 来源:发表于2021-05-13 23:01 被阅读0次

Jetpck 才是真的豪华全家桶

引言

  • DataBinding 实现了视图与数据的双向绑定,代码简洁,Activity 中代码不再因此膨胀。
  • DataBinding 带来了软件架构的新模式 MVVM
  • 如果使用 DataBinding 的主要目的是取代 findViewById() 调用,请考虑改用 ViewDataBinding。
  • 灵活强大的框架,会带来学习成本的提升,深度掌握它,做工具的主人。

整体预览

Jetpack DataBinding 概览图

用图说话

 文章较长,考虑到心急的宝宝们看不到最后,所以总结性的图,放到最前面吧!

1. DataBinding 数据流向 生成绑定类

Jetpack DataBinding 数据流向 生成绑定类

2. DataBinding 数据流向 绑定类相互关系

Jetpack DataBinding 数据流向 绑定类相互关系

3. DataBinding 数据流向 普通-数据对象

Jetpack DataBinding 数据流向 普通-数据对象

4. DataBinding 数据流向 可观察-数据对象

Jetpack DataBinding 数据流向 可观察-数据对象

5. DataBinding 数据流向 双向数据绑定

Jetpack DataBinding 数据流向 双向数据绑定

6. DataBinding 数据流向 横向对比

Jetpack DataBinding 数据流向 横向对比

1. 语法说明

1.1 环境配置

  模块启用

//build.gradle
android {
        buildFeatures {
            dataBinding true
        }
    }

1.2 布局文件格式

1.2.1 数据绑定布局文件

 以根标记 layout 开头,后跟 data 元素和 view 根元素。绑定视图的根其实是View根元素。

<?xml version="1.0" encoding="utf-8"?>
<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>

        //data 中的 user 变量描述了可在此布局中使用的属性。
        <variable
            name="user"
            type="com.kejiyuanren.jetpack.databinding.ViewModel" />
    </data>

    //布局中的表达式使用“@{}”语法写入特性属性中。
    //在这里,TextView文本被设置为 user 变量的 userName 属性
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".databinding.DataBindingActivity">

        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:text="@{user.userName}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Tips:布局表达式应保持精简,因为它们无法进行单元测试,并且拥有的 IDE 支持也有限。

1.2.2 数据对象

 定义一个最简单的数据对象。

data class ViewModel(val userName:String)

1.3 生成绑定类

 生成的绑定类将布局变量与布局中的视图关联起来。所有生成的绑定类都是从 ViewDataBinding 类继承而来的。系统会为每个布局文件生成一个绑定类(绑定类名称规则:布局文件名为 activity_main.xml,因此生成的对应类为 ActivityMainBinding)。

1.3.1 创建绑定对象

 创建绑定类的方式有很多种。

class DataBindingActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //普通实现
//        setContentView(R.layout.activity_data_binding2)

        //绑定实现 1
        val binding: ActivityDataBinding2Binding = DataBindingUtil.setContentView(
            this,
            R.layout.activity_data_binding2
        )
        binding.user = ViewModel("kejiyuanren - 1")

        //绑定实现2(其实binding类的生成,主要就是 DataBindingUtil类 和 xxxBinding类 中的实现)
//        val binding2: ActivityDataBinding2Binding =
//            ActivityDataBinding2Binding.inflate(layoutInflater)
//        setContentView(binding2.root)
//        binding2.user = ViewModel("keyijiyuanren-2")
    }
}

Tips:绑定类创建汇总(xxxBinding最终依然会调到 DataBindingUtil)。

创建绑定对象汇总
1.3.2 带 ID 的视图

 数据绑定库会针对布局中具有 ID 的每个视图在绑定类中创建不可变字段。相较于针对布局中的每个视图调用 findViewById() 方法,这种机制速度更快。如果没有数据绑定,则 ID 并不是必不可少的,但仍有一些情况必须能够从代码访问视图。

1.3.3 变量

 数据绑定库为布局中声明的每个变量生成访问器方法( setter 和 getter )。

//xml code
<layout ……>

    <data>

        <variable
            name="star"
            type="int" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        ……>
        <TextView
            ……
            android:text="@{String.valueOf(star)}" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

//java code
binding.setStar(5)
1.3.4 ViewStub

 ViewStub是可用于延迟加载视图的组件。在DataBinding中的用法如下:

//xml code -> activity.xml
<layout ……>

    <androidx.constraintlayout.widget.ConstraintLayout
        ……>
        <ViewStub
            android:layout="@layout/view_stub_tip"
            …… />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

//xml code -> view_stub_tip.xml
<layout ……>

    <data>
        <variable
            name="tipModel"
            type="com.kejiyuanren.jetpack.databinding.TipViewModel" />
    </data>

    <TextView
        android:text="@{tipModel.tipInfo}"
        ……/>
</layout>

//java code
fun viewStubAdd() {
    //视图扩展监听器, 获取已经生成的绑定类,并执行更新操作
    binding.viewStubExpand.setOnInflateListener { _, inflated ->

        //OK : 方式 1,直接获取了view的binding缓存(因为binding已经生成过了,在ViewStubProxy中)
        val vb1: ViewStubTipBinding? = DataBindingUtil.bind(inflated)

        //OK : 方式 2,直接获取 ViewStubProxy中的 binding缓存
//            val vb2 = binding.viewStubExpand.binding as ViewStubTipBinding

        //ERROR : 方式 3, 直接通过扩展layout的绑定类生成,因为在ViewStubProxy中已经创建过了
        //创建过的binding类会清空view对应的tag, 所以会报错(view must have a tag)
        //这种机制也保证了,binding类的单例特性
//            val vb3 = ViewStubTipBinding.bind(inflated)

        vb1?.tipModel = TipViewModel("666")
    }

    //延迟5s触发加载扩展视图
    binding.root.postDelayed({
        val vs: ViewStub? = binding.viewStubExpand.viewStub
        vs?.inflate()
    }, 5000)
}
1.3.5 动态变量

 有时,系统并不知道特定的绑定类。例如,针对任意布局运行的 RecyclerView.Adapter 不知道特定绑定类。在调用 onBindViewHolder() 方法时,仍必须指定绑定值。比如:RecyclerView 绑定到的所有布局都有 itemModel 变量。

//xml code
<layout ……>
    <data>
        <variable
            name="itemModel"
            type="com.kejiyuanren.jetpack.databinding.ItemViewModel" />
    </data>

    <TextView
        android:text="@{itemModel.name}"
        …… />
</layout>

//java code
import com.kejiyuanren.jetpack.BR   //记得要引入 BR,不然会报错(Unresolved reference: BR)
class RvAdapter(private val mData: List<ItemViewModel>) :
    RecyclerView.Adapter<RvAdapter.RvViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RvViewHolder {
        val db = ItemDbRvTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return RvViewHolder(db)
    }

    override fun getItemCount(): Int {
        return mData.size
    }

    override fun onBindViewHolder(holder: RvViewHolder, position: Int) {
        holder.vb.setVariable(BR.itemModel, mData[position])
        //当可变或可观察对象发生更改时,绑定会按照计划在下一帧之前发生更改。
        //但有时必须立即执行绑定。要强制执行,请使用 executePendingBindings()` 方法。
        holder.vb.executePendingBindings() 
    }

    class RvViewHolder(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
        val vb = binding
    }
}
1.3.6 自定义绑定类名称

 默认情况下,绑定类是根据布局文件的名称生成的。通过调整 data 元素的 class 特性,绑定类可重命名或放置在不同的包中。

//xml code
<layout ……>
    <data class="KeJiYuanRen">
    </data>
</layout>

//java code
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //绑定类已经变为了自定义,不再是默认的命名规则类
        val binding: KeJiYuanRen = DataBindingUtil.setContentView(
            this,
            R.layout.activity_data_binding2
        )
    }

1.4 绑定适配器

 绑定适配器负责发出相应的框架调用来设置值。

1.4.1 自动选择方法

 您可以使用数据绑定为任何 setter 创建特性。以 android:text="@{user.name}" 表达式为例,库会查找接受 user.getName() 所返回类型的 setText(arg) 方法。如果 user.getName() 的返回类型为 String,则库会查找接受 String 参数的 setText() 方法。

1.4.2 指定自定义方法名称

 一些属性具有名称不符的 setter 方法(比如TextView中就没有setText(arg : Int))。那么就可以自定义方法名称,使用 BindingMethods注释与 setter 相关联。注释与类一起使用,可以包含多个 BindingMethod注释,每个注释对应一个重命名的方法。

//xml code
<layout ……>
    <data>
        <variable
            name="star"
            type="int" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        ……>
        <com.kejiyuanren.jetpack.databinding.Number
            app:number="@{star}"
            ……/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

//java code
@BindingMethods(    //自定义方法名称集合,可以包含多个BindingMethod
    value = [       //这是个数组
        BindingMethod(
            type = TextView::class,  //要操作的属性属于哪个类
            attribute = "number",    //xml属性,使用(app:number=“20”)
            method = "setTextNumber" //指定xml属性对应的set方法,参数类型要对应
        )]
)

class Number @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) {

    fun setTextNumber(number : Int) {
        text = number.toString()
    }
}
1.4.3 提供自定义逻辑

 一些属性需要自定义绑定逻辑。参数类型非常重要。第一个参数用于确定与特性关联的视图类型,第二个参数用于确定在给定特性的绑定表达式中接受的类型。

//xml code
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

//java code
  //如果不需要同时满足,则requireAll设置为false
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = true) 
    fun loadImage(view: ImageView, url: String, error: Drawable) {
        Picasso.get().load(url).error(error).into(view)
    }
1.4.4 自动转换对象

 当绑定表达式返回 Object 时,库会选择用于设置属性值的方法。如果参数类型不明确,则必须在表达式中强制转换返回类型。

1.4.5 自定义转换

 在某些情况下,需要在特定类型之间进行自定义转换。转换器是全局的,请谨慎使用。

//xml code
<layout ……>
    <data>
        <variable
            name="show"
            type="boolean" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        ……>
        <TextView
            android:visibility="@{show}"
            ……/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

//java code
object DbAdapter {
    @BindingConversion
    @JvmStatic
    fun setShowView(show: Boolean): Int {
        return if (show) {
            View.VISIBLE
        } else {
            View.GONE
        }
    }
}

2. 扩展语法

2.1 布局和绑定表达式

2.1.1 表达式语言

 表达式语言与托管代码中的表达式非常相似。

2.1.1.1 Null 合并运算符

//传统方式
android:text="@{user.displayName != null ? user.displayName : user.lastName}"

//Null 合并运算符方式
android:text="@{user.displayName ?? user.lastName}"

2.1.1.2 避免出现 Null 指针异常
 生成的数据绑定代码会自动检查有没有 null 值并避免出现 Null 指针异常。例如,在表达式 @{user.name} 中,如果 user 为 Null,则为 user.name 分配默认值 null。如果引用 user.age,其中 age 的类型为 int,则数据绑定使用默认值 0。

2.1.1.3 视图引用
 表达式可以通过语法(绑定类将 ID 转换为驼峰式大小写),按 ID 引用布局中的其他视图。

//TextView 视图引用同一布局中的 EditText 视图
<EditText
        android:id="@+id/example_text"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"/>
    <TextView
        android:id="@+id/example_output"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{exampleText.text}"/>

2.1.1.4 集合
 为方便起见,可使用 [] 运算符访问常见集合,例如数组、列表、稀疏列表和映射。

<data>
        <import type="java.util.List"/>
        <variable name="list" type="List&lt;String>"/>
        <variable name="index" type="int"/>
    </data>
    …
    android:text="@{list[index]}"

2.1.1.5 字符串字面量

//可以使用单引号括住特性值,这样就可以在表达式中使用双引号
android:text='@{map["firstName"]}'

//也可以使用双引号括住特性值。如果这样做,则还应使用反单引号 ` 将字符串字面量括起来
android:text="@{map[`firstName`]}"

2.1.1.6 资源

//简单
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

//提供参数来评估格式字符串和复数形式
android:text="@{@string/nameFormat(firstName, lastName)}"
2.1.2 事件处理

 通过数据绑定,您可以编写从视图分派的表达式处理事件(例如,onClick() 方法)。

2.1.2.1 方法引用

//xml code
<layout ……>
    <data>
        <variable
            name="click"
            type="com.kejiyuanren.jetpack.databinding.ClickHandler" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        ……>
        <TextView
            android:onClick="@{click::onBtnClick}"
            ……/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

//java code
class ClickHandler {
    fun onBtnClick(view: View) {
        Log.d(TAG, "onBtnClick: ")
    }
}

2.1.2.2 监听器绑定
 监听器绑定是在事件发生时运行的绑定表达式。它们类似于方法引用,但允许运行任意数据绑定表达式。

//总有一款适合你
android:onClick="@{() -> presenter.onSaveClick(task)}"
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
2.1.3 导入、变量和包含

2.1.3.1 导入
 通过导入功能,您可以轻松地在布局文件中引用类,就像在托管代码中一样。可以在 data 元素使用多个 import 元素,也可以不使用。

<data>
        <import type="com.kejiyuanren.jetpack.databinding.View"
            alias="UnitView"/>  //类型别名,防止与下面的冲突
        <import type="android.view.View"/>    //引入View
    </data>

<TextView
       android:text="@{user.lastName}"
       android:text="@{((User)(user.connection)).lastName}"  //类型强转
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>  //使用View

2.1.3.2 变量
 您可以在 data 元素中使用多个 variable 元素。

2.1.3.3 包含
 通过使用应用命名空间和特性中的变量名称,变量可以从包含的布局传递到被包含布局的绑定。注意:数据绑定不支持 include 作为 merge 元素的直接子元素。

//activity.xml
<layout ……>
    <data>
        <variable
            name="user"
            type="com.kejiyuanren.jetpack.databinding.ViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        ……>
        <TextView
            android:text="@{user.userName}"
            …… />
        <include
            layout="@layout/layout_title_append"
            app:user="@{user}"
            ……/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

//layout_title_append.xml
<layout ……>
    <data>
        <variable
            name="user"
            type="com.kejiyuanren.jetpack.databinding.ViewModel" />
    </data>
    <TextView
        android:text="@{user.userName.toUpperCase()}"
        ……>
    </TextView>
</layout>

2.2 可观察的数据对象

 可观察性是指一个对象将其数据变化告知其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察。

2.2.1 可观察字段

在创建实现 Observable 接口的类时要完成一些操作,但如果您的类只有少数几个属性,这样操作的意义不大。字段设为可观察字段:ObservableXXX。

//数据模型
class ViewModel {
    val userName = ObservableField<String>()
}

//观察更新
        val userModel = ViewModel()
        userModel.userName.set("keyijiyuanren-1")
        binding.user = userModel

        binding.click = ClickHandler(object : ClickListener{
            override fun onClick() {
                userModel.userName.set("replace = $count")
            }
        })
2.2.2 可观察对象

实现 Observable 接口的类允许注册监听器,以便它们接收有关可观察对象的属性更改的通知。

//数据模型
class ViewModel : BaseObservable() {
    @get:Bindable
    var userName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.userName)
        }
}

//观察更新
        val userModel = ViewModel()
        userModel.userName = "keyijiyuanren-1"
        binding.user = userModel

        binding.click = ClickHandler(object : ClickListener{
            override fun onClick() {
                userModel.userName = "replace = $count"
            }
        })

2.3 数据双向绑定

 双向绑定可以在上面的 2.2.1 可观察字段基础上,在xml中使用语法:@={} 表示法(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。

//xml code
<CheckBox
    android:checked="@={checkModel.check}"
    …… />

//java code
class CheckModel {
    val check = ObservableBoolean(false)
}
2.3.1 使用自定义特性的双向数据绑定

&emsp;最常见的双向特性和更改监听器提供了双向数据绑定实现,可以将其用作应用的一部分。如果希望结合使用双向数据绑定和自定义特性,则需要使用 @InverseBindingAdapter@InverseBindingMethod 注释。
 下面实现了SeekBar的透明度与进度随动。

//xml code
<layout ……>
    <data>
        <variable
            name="defineAlpha"
            type="float  " />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        ……>
        <SeekBar
            app:okAlpha="@={defineAlpha}"
            …… />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

//java code
    //第三步:获取到已经更新了的defineAlpha数据,并更新 okAlpha 的 BindingAdapter方法
    @BindingAdapter("okAlpha")
    @JvmStatic
    fun setOkAlpha(okBar: SeekBar, newValue: Float) {
        okBar.alpha = newValue
    }

    //第二步:滑动触发 okAlpha 的 InverseBindingAdapter方法调用,获取属性okAlpha的值,更新defineAlpha的数据
    @InverseBindingAdapter(attribute = "okAlpha")
    @JvmStatic
    fun getOkAlpha(okSeekBar: SeekBar): Float {
        return okSeekBar.progress / 100f
    }

    @BindingAdapter("app:okAlphaAttrChanged")
    @JvmStatic
    fun setListeners(
        okSeekBar: SeekBar,
        attrChange: InverseBindingListener
    ) {
        // Set a listener for click, focus, touch, etc.
        okSeekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                //第一步:监听滑动,会触发 okAlphaAttrChanged 的属性 okAlpha 的 InverseBindingAdapter方法
                attrChange.onChange()
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {

            }

            override fun onStopTrackingTouch(seekBar: SeekBar?) {

            }
        })
    }
2.3.2 转换器

 如果绑定到 View对象的变量需要设置格式、转换或更改后才能显示,则可以使用 Converter 对象。较简单。

2.3.3 使用双向数据绑定的无限循环

 使用双向数据绑定时,请注意不要引入无限循环。当用户更改特性时,系统会调用使用@InverseBindingAdapter 注释的方法,并且该值将分配给后备属性。继而调用使用 @BindingAdapter 注释的方法,从而触发对使用 @InverseBindingAdapter 注释的方法的另一个调用,依此类推。
 因此,通过比较使用 @BindingAdapter 注释的方法中的新值和旧值,可以打破可能出现的无限循环。

2.3.4 双向特性

 平台对部分控件提供对双向数据绑定的内置支持。比如 TextViewBindingAdapter

2.4 布局绑定架构组件

 数据绑定库可与架构组件(AndroidX 库包含的架构组件)无缝协作,进一步简化界面的开发。在后面的ViewModel和LiveData中再展开。

3. DataBinding文件说明

3.1 绑定类路径

3.1.1 JavaModel 视图层

 参考 Jetpack ViewBinding

3.1.2 JavaModel 数据层

 路径:app/build/generated/source/kapt/buildTypes

绑定类 数据层 生成路径
3.1.3 Layout文件

 参考 Jetpack ViewBinding

3.2 绑定类作用

3.2.1 JavaModel 视图层

 JavaModel 视图层(抽象类),继承自 ViewDataBinding,功能类似ViewBinding。参考 Jetpack ViewBinding

3.2.2 JavaModel 数据层

 JavaModel 数据层,继承自 视图层,添加的数据绑定的功能。

  • BR:绑定属性的字段。
  • DataBindingComponent:绑定类作用域。
  • DataBinderMapperImpl:
    • androidx.databinding 路径下:全局缓存添加器。
    • com.xx.xx(包名)路径下:全局缓存器。
  • XxxBindingImpl:绑定类的字段更新策略,绑定类的生成单例策略等。
3.2.3 Layout文件

 用于生成JavaModel(绑定类)。参考 Jetpack ViewBinding

4. DataBinding原理分析

4.1 布局绑定类生成

 参考 Jetpack ViewBinding

4.2 数据绑定类生成

 参考 Jetpack ViewBinding

4.3 绑定功能的数据流向

 直接来个最全的吧。双向绑定数据流向:更新数据刷新视图 vs 更新视图刷新数据。

 例子代码:

//xml code
<layout ……>
    <data>
        <variable
            name="checkModel"
            type="com.kejiyuanren.jetpack.databinding.CheckModel  " />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        ……>
        <CheckBox
            android:checked="@={checkModel.check}"
            …… />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

//java code
class CheckModel {
    val check = ObservableBoolean(false)
}

//java 更新操作
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityDataBinding2Binding = DataBindingUtil.setContentView(
            this,
            R.layout.activity_data_binding2
        )

        //设置java模型
        var checkModel = CheckModel()
        binding.checkModel = checkModel

        binding.click = ClickHandler(object : ClickListener{
            override fun onClick() {
                //辅助button,点击,执行反向选择设置
                checkModel.check.set(!checkModel.check.get())
            }
        })
    }

 流程图示:


Jetpack DataBinding 数据流向 绑定数据更新

5.小结

 DataBinding 催生了 MVVM,代码简洁松耦合。JetPack-Compose 才是大Boss,比DB还DB。不过目前还没有稳定版,期待……

小编的博客系列

Jetpack 全家桶

相关文章

网友评论

    本文标题:Jetpack DataBinding

    本文链接:https://www.haomeiwen.com/subject/baxhrltx.html