Android Jetpack安卓

Jetpack DataBinding

2021-05-13  本文已影响0人  科技猿人

Jetpck 才是真的豪华全家桶

引言

整体预览

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 数据层,继承自 视图层,添加的数据绑定的功能。

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 全家桶

上一篇下一篇

猜你喜欢

热点阅读