Android 界面库 (二) 之 Data binding 详细介绍

news2024/11/20 3:37:06

1. 简介

        回顾我们在前面文章《Android 界面库 (一) 之 View binding 简单使用》中学习的 View Binding,它旨在简化 View 与代码之间的绑定过程。它会在编译时期为每个 XML 布局文件生成相应的绑定类(Binding class),该类里包含了布局文件每个有 ID 的 View 的引用,从而避免了频繁去手动调用 findViewById() 方法获取 View 对象。

        本篇文章将会学习 View binding 的进阶版本的界面库技术,它就是 Data binding。Data binding 也是 Android Jetpack 库的一部分,它跟 View binding 一样会在编译时期为布局文件生成对应的绑定类。不仅如此,它还有较为高级的功能,就是在布局中绑定数据。

        Data binding 通常用于将 UI 布局元素与逻辑端数据模型之间建立连接。这样 UI 元素便可自动与数据模型的值进行同步更新,从而实现 UI 与数据的绑定。通过这种方式则可以让开发者更专注数据和业务逻辑,而不必过多关注 UI 的更新。

2. 启用 Data binding

        如果需要在工程项目中启用Data binding,需要先在项目模块级 buid.gradle 文件中将 dataBinding 构建选项设置为 true, 如:

android {
    ...
    buildFeatures {
        dataBinding true
    }
}

3. 使用

3.1. XML布局

        Data binding 的 XML 布局文件跟常规布局文件略有不同,Data binding 布局的根标记以 layout 开头,后跟 data 元素,随后才是原来的常规非绑定布局文件中的根 View。示例布局:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.name}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{ String.valueOf (user.age)}"/>
   </LinearLayout>
</layout>

其中,data 中的 variable 用于在布局中定义变量,这里的变量是 com.example.User 类型的 user。即示例中:

<variable name="user" type="com.example.User" />

布局中的表达式使用 @{} 语法写入属性。即示例中:

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.name}" />

提示:文章下方会详细介绍变量和表达式。

Android Studio自动转成 Data binding布局:

        当你将鼠标移到布局文件中的根元素时,会出现一个黄色灯提示悬浮按钮,点击它然后选择Convert to data binding layout,那么 Android Studio 会把常规布局自动转成 Data binding 绑定布局。如下图所示:

3.2. 布局的变量

        如上述示例,在布局文件中的 data 元素里的 variable 元素就是用于定义布局上的变量。variable 元素可以有多个,它可以在布局文件中的绑定表达式中使用。示例: 

<data>
    <variable name="user" type="com.example.User"/>
    <variable name="name" type="String"/>
    <variable name="age" type="int"/>
</data>

注意:如果各种配置(例如横向或纵向)有不同的布局文件,系统会合并变量。这些布局文件之间不能有冲突的变量定义。

3.2.1 导入类包

        在布局文件中的 data 元素里,可以像 Java/Ktolin 代码导入包一样使用 import 来导入引用类,示例:

<data>
    <import type=" com.example.User "/>
    <import type="java.util.List"/>
    <import type="android.view.View"/>
    <import type="com.example.real.estate.View" alias="Vista"/>
    <import type="com.example.MyStringUtils"/>

    <variable name="user" type=" User"/>
    <variable name="userList" type="List&lt;User>"/>
</data>
  1. 通过导到入了 User 类,那么在下面的 variable 元素定义变量时便可以不再使用完整的类名;
  2. 通过导入 View 类,便可以从下方绑定表达式中引用该类。例如使用 View 类的 VISIBLE 和 GONE 常量:
  3. 当导入的类名冲突时,还可以使用 alias 来给导入的类重命名,然后便可以在布局文件中使用重命名后的类名来引用它字段或方法;
  4. 还可以导入某一个类,然后在表达式中使用该类的静态方法。

使用导入类的表达式示例:

<TextView
   android:text="@{user.name}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.age >=18 ? View.VISIBLE : View.GONE}"/>

<TextView
   android:text="@{MyStringUtils.capitalize(user.name)}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

3.2.2 传递变量给子布局

        你可以将变量从包含布局传递到所含子布局的绑定中,方法是在属性中使用应用命名空间和变量名称。示例:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/name_layout"  bind:user="@{user}"/>
   </LinearLayout>
</layout>

name_layout.xml

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

        示例中在 activity_main.xml 通过 bind:user="@{user}" 将 user 变量传递到 name_layout.xml布局文件中,这样就可以在name_layout.xml布局中也使用user变量。

注意:在主布局和子布局中,变量名称和类型必须一致。否则,数据绑定将无法识别并传递变量。

特别注意:Data binding不支持 include 作为 merge 元素的直接子元素。例如以下是一个错误的示例:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <merge>
       <!-- Doesn't work -->
       <include layout="@layout/name_layout"  bind:user="@{user}"/>
   </merge>
</layout>

3.3. 布局的表达式

        如上述示例,在布局 View 中使用的 @{…},便是布局中的表达式。布局表达式有它自己的语言规则,例如它能支持运算符和一些特定的关键字。

3.3.1. 常见表达式运算符和关键字

类型

操作符

数字

+ - / * %

字符串串联

+

逻辑

&& ||

二进制文件

& | ^

二进制位移

>> >>> <<

一元组

+ - ! ~

比较

== > < >= <= (“<”是XML语法关键字,所以需要转义为 &lt;)

数组访问

[ ]

三元运算符

?: (跟Java中的“?:” 一样用法,用作条件判断后选择)

Null 合并

?? (跟Kotlin中的“?:”一样用法,用作前值为空时使用后值)

示例:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 18 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
android:text="@{user. firstName ?? user.lastName}"
android:text="@{user. firstName != null ? user. firstName: user.lastName}"

注意:任何在布局中的表达式返回值都需要使用 String.valueOf 转换成字符串类型,正如示例,如果存在这样 android:text="@{index + 1}" 是会报异常的。

自动避免空指针:

        生成的Data binding 代码会自动检查 null 值并避免空指针异常。例如,在表达式 @{user.name} 中,如果 user 为 null,则会为user.name分配其默认值 null。如果引用 user.age,其中 age 的类型为 int,则会使用默认值 0。

建议:一般应让布局表达式小而简单,因为它们无法进行单元测试,并且 IDE 支持也有限。

3.3.2. View 引用

        表达式支持使用按 ID 引用布局中的其他View,示例:

<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}"/>

3.3.3. 集合

        可以使用 [ ] 运算符访问常见集合,例如数组、列表、稀疏列表和映射。使用这些集合时,需要对其指定泛型类型。示例:

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

“<”是XML关键字,为确保的语法正确, “<”字符是需要进行转义成&lt; 。如上述示例中的 List&lt;String>,而不是 List<String>。

3.3.4. 字符串和资源

        可以使用英文单引号括住属性值,这样就可以在表达式中使用英文双引号,或者使用双引号括住属性值,字符串字面量用反引号 ` 括起来。

        资源的引用,可使用 @xx/xx的语法示例:

android:text="@{age == 0 ? `零` :  @string/no_zero }"
...
android:text='@{age == 0 ? "零" :  @string/no_zero }'
...	
// 带参数的资源
android:text="@{@string/example_resource(user.name, exampleText.text)}"

3.3.5. 默认值

        如果在初始阶段,表达式引用的变量并未初始化完成,这时你又不希望界面显示出空的值,可以使用默认值显示。示例:

<TextView 
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{user.name, default=my_default}"/>

 但是,如果你仅仅是需要在项目的设计阶段显示默认值,则可以使用 tools 属性,而不是默认表达式值。示例:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="my_default"/>
</LinearLayout>

3.4. 布局的事件处理

3.4.1 方法引用

        在表达式中,可以引用符合 Listener 方法签名的方法。方法与 android:onClick  分配给 activity 中的方法类似。与 View onClick 属性相比优点是表达式在编译时得到处理。因此如果该方法不存在或其签名不正确,会在编译时期提前知道。示例:

创建 MyHandlers 类:

class MyHandlers {
    // 方法签名必须跟监听器方法签名完全匹配
    fun onClickFriend(view: View) { ... }
}

布局: 

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.name}"
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

3.4.2. 监听器绑定

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

        在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只有返回值与监听器的返回值一致即可,除非预期返回值为 void。例如,假设存在以下presenter 类:

class Presenter {
    fun onSaveClick(task: Task){}
    fun onSaveClick2(view: View, task: Task){}
    fun onCompletedChanged(task: Task, completed: Boolean){}
    fun onLongClick(view: View, task: Task): Boolean { }
}

可以将事件绑定到presenter类的方法,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="task" type="com.android.example.Task" />
        <variable name="presenter" type="com.android.example.Presenter" />
    </data>
    <LinearLayout 
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--忽略方法的所有参数--> 
        <Button 
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> presenter.onSaveClick(task)}" />

        <!--命名所有参数,并使用参数-->
        <Button 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:onClick="@{(view) -> presenter.onSaveClick2(view, task)}" />
        <CheckBox 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

        <!--带返回值的表达式,如果表达式因presenter对象为空导致而无法求值,则会返回该类型的默认值,这里是Boolean则会返回false--> 
        <Button 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:onLongClick="@{(view) -> presenter.onLongClick(view, task)}" />

        <!--三元表达式,可使用 void --> 
        <Button 
            android:layout_width="wrap_content" 
            android:layout_height="wrap_content"
            android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

    </LinearLayout>
</layout>

建议:虽然监听器表达式功能强大,可让你的代码更易于阅读。但另一方面,若表达式过于复杂的监听器也会使布局变得更难以阅读和维护。所以请尽量让表达式保持简单,避免使用复杂的监听器。
 

3.5. 生成绑定类

        跟 View binding 一样,当你工程 Gradle 中配置启用 Data binding 后,在工程编译阶段就会为每个布局文件生成对应的绑定类,其中类的默认命名规则是:XML 文件的名称转换为 Pascal 命名规则的大小写形式,并在末尾添加“Binding”。例如,布局文件名为 activity_main.xml,生成的对应绑定类为 ActivityMainBinding。该类位于模块包下的 databinding 包中。例如模块包名为com.example.app,则绑定类的全称就是:com. example.app.databinding.ActivityMainBinding 。

3.5.1. 自定义绑定类名称

        可以通过配置 data 元素的 class 属性来重命名绑定类。如:

<data class=”myData”>
     …
</data>

如果你希望将生成的绑定类放置在自定义的包下可以这样:

<data class=”com.example.app.abc.myData”>
     …
</data>

3.5.2. 代码中创建绑定对象

        生成的绑定类继承自 ViewDataBinding 类,里除了包含了布局文件每个有 ID 的 View 的引用外,还会包含布局中定义的变量引用。每个布局变量都有一个对应的 setter 和 getter。在调用 setter 之前,这些变量会采用默认的托管代码值 null 用于引用类型,0 用于 int,false 用于 boolean,等等。
        假设 com.example.User 类定义如下:

data class User(val name: String, val age: Int)

在 Activity 的 onCreate 方法中,可以执行以下代码来绑定 Activity 和布局文件,以及为布局中的变量赋值:

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    binding.user = User("子云心", 18)
}
或者也可以使用跟 View binding 一样的 LayoutInflater 方式:
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)

        如果在 Fragment、ListView 或 RecyclerView 的Adapter 内使用 Data binding项,则需要使用绑定类的 inflate() 方法或 DataBindingUtil 类,示例:

val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)

或者:        

val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

完整的 RecyclerView 的 Adapter 示例:

class MyRecyclerViewAdapter(private val userList: ArrayList<User>, private val context: Context)
    : RecyclerView.Adapter<MyRecyclerViewAdapter.MyViewHolder>() {
    class MyViewHolder(val listItemBinding: ListItemBinding) : RecyclerView.ViewHolder(listItemBinding.root) {
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val databind = DataBindingUtil.inflate<ListItemBinding>(LayoutInflater.from(context), R.layout.list_item, parent, false)
        val holder = MyViewHolder(databind)
        holder.listItemBinding.itemName.setOnClickListener {
            val position = holder.getAbsoluteAdapterPosition()
            Toast.makeText(it.context, "click ${userList[position].name}", Toast.LENGTH_SHORT).show()
        }
        return holder
    }
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.listItemBinding.item = userList[position]
    }
    override fun getItemCount(): Int {
        return userList.size
    }
}

4. 单向数据绑定

        单向绑定就是将数据模型绑定到用户界面(UI),使得当数据模型的值变化时,UI能够自动更新以反映这些变化。但是 UI 上的更改不会影响数据模型发生改变。这种方式适用于需要将数据动态地显示在 UI 上的场景,例如仅将某些信息显示在TextView中。

        正常情况下,数据对象的改变是不会自动更新 UI 的,所以如果要实现自动更新机制,就要让数据对象实现监听器,在发生更改时通知其它对象,而使用可观察的数据对象绑定到界面时,当其数据发生更改时,UI 就可以自动更新。

        可观察的数据类实现方式有三种分别是:字段、集合和对象。

4.1. 可观察的数据对象

4.1.1. 可观察字段

        可观察字段就是将数据对象类中的字段的普通类型更改为 Observable 系列的类型,Observable 系列的类型有:

  • ObservableBoolean
  • ObservableByte
  • ObservableChar
  • ObservableShort
  • ObservableInt
  • ObservableLong
  • ObservableFloat
  • ObservableDouble
  • ObservableParcelable<T>
  • ObservableField<T>

        将上面的data class User 使用 Observable 进行一下改造:

class UserObservable {
    val name = ObservableField<String>()
    val age = ObservableInt()
}

注意字段最好使用 val 表示为只读属性,因为它不需要再被修改,访问字段值时,就需要使用 set() 和 get() 访问器方法。如给布局变量赋值:

binding.userObservable = UserObservable().apply {
    name.set("子云心")
    age.set(18)
}

这时,如果布局直接绑定了UserObservable对象,那么就会自动更新。布局如下:

<data>
   <variable name="userObservable" type="com.example.UserObservable"/>
</data>
...
<TextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{ userObservable.name }"/>

这样就可以不再需要去了调用以下代码:

binding.tvName.setText(“子云心”)

4.1.2. 可观察集合

        可观察集合就是将普通的 List、ArrayList、Map、ArrayMap 变成 Observable 系列集合:ObservableList、ObservableArrayList、ObservableMap、ObservableArrayMap 等。示例:

binding.userMap = ObservableArrayMap<String, Any>().apply {
    put("name", "子云心")
    put("age", 18)
}

布局中将 user 变量换成 ObservableMap 类型的 userMap:

<data>
       <import type="androidx.databinding.ObservableMap"/>
       <variable name="userMap" type="ObservableMap&lt;String, Object>"/>
</data>
...

<TextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{String.valueOf(userMap.get(`name`))}"/>

4.1.3. 可观察对象

        可观察对象就是让对象的类实现 BaseObservable 接口,类中的属性的 getter 需要分配一个 Bindable 注解,并在 setter 中调用 notifyPropertyChanged() 方法,这样类在属性发生更改时会发出通知。

        将上面的 UserObservable 使用 BaseObservable 再进行一下改造:

class UserObservable: BaseObservable() {
    @get:Bindable
    var name: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(com.example.BR.name)
        }
    @get:Bindable
    var age: Int = 0
        set(value) {
            field = value
            notifyPropertyChanged(com.example.BR.age)
        }
}

给布局变量赋值:

binding.userObservable = UserObservable().apply {
    name = "子云心"
    age = 18
}

布局变量就是 BaseObservable 类型的 userObservable:

<data>
   <variable name=" userObservable " type="com.example. UserObservable "/>
</data>
...

<TextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>

        上述示例中的 BR 类是在编译时生成在模块包下的类,其中包含用于 Data binding 的资源的 IDBindable 注解会在编译期间在 BR 类文件中生成一个条目。

注意:如果在编译期间报:Unresolved reference: BR 错误,需要往模块Gradle中进行 kotlin-kapt 插件配置,如:

plugins {
    ...
    id 'kotlin-kapt'
}

4.1.4. 生命周期感知型对象

        在布局中其实还可以直接绑定到数据绑定来源,数据绑定来源会自动通知界面有关数据的变化。这样绑定就能够感知生命周期,并且仅在界面显示在屏幕上时才会触发。数据绑定支持 StateFlowLiveData

提示:关于 StateFlow 和 LiveData 会在后面的 MVVM 系列文章中再详细介绍。

4.2. @BindingAdapter (绑定适配器注解)

        BindingAdapter 绑定适配器是 DataBinding 中用于扩展布局 XML 属性行为的注解,它可以支持布局 XML 中的一个或多个属性进行绑定行为扩展。而且注解值可以是已存在的属性,如android:text,也可以是自定义属性,如:app:imageUrl。虽然其名称叫适配器,实际使用上它更像是一个拦截器。

        BindingAdapter 注解和方法可定义在代码任意地方,注解接收一个字符串参数,用于指定 XML 中的属性名,注解对应的方法必须是一个静态方法(如果你使用 Kotlin 语言,那么需要在包级函数、单例(object)类或伴生对象(companion object)内的方法需另外增加 @JvmStatic 注解),方法名称可以随意定义,方法不需要返回值,第一参数用于确定与属生关联的View的类型,第二个参数用于确定给定属性的绑定达表式中接收的类型。

4.2.1. 已存在属性

布局:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{`子云心`}" />

注解和方法:

@JvmStatic
@BindingAdapter("android:text")
fun setText(view: TextView, txt: String?) {
    view.text = "BindingAdapter_$txt"
}

        示例中 TextView 最终展示的值是:“BindingAdapter_子云心”

4.2.2. 自定义属性

布局:

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:imageUrl="@{`https://www.xxx.com/xxx.png`}"/>

注解和方法:

@JvmStatic
@BindingAdapter("imageUrl")
fun setImageUrl(view: ImageView, url: String?) {
    Picasso.get().load(url).into(view)
}

        示例中自定义了一个名为 imageUrl 的属性,在 setImageUrl 方法中通过工作器线程调用自定义加载器来加载网络图片。

4.2.3. 同时多个自定义属性

布局:

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:imageUrl="@{`https://www.xxx.com/xxx.png`}"
    app:error="@{@drawable/venueError }"/>

注解和方法:

@JvmStatic
@BindingAdapter("imageUrl", "error")
fun loadImage(img: ImageView, url: String?, error:Drawable?) {
    Picasso.get().load(url).error(error).into(view)
}

        上述示例在布局的 XML 中使用的自定义属性,必须同时提供 imageUrl 和 error 绑定方法才能生效被触发。如果你希望只要提供其中一个自定义属性也可以触发绑定逻辑,那么可以在注解方法中增加 requireAll 标志并设置为 false,如:

@JvmStatic
@BindingAdapter(value = ["imageUrl", " error "], requireAll = false)
fun loadImage(img: ImageView, url: String?, error:Drawable?) {
    if(url != null) {
        ……
    } else if (error != null) {
        ……
    } else {
        ……
    }
}

4.2.4. 接受旧值

        绑定适配器方法可以在其处理程序中接受旧值。同时接受旧值和新值的方法参数必须先旧值,随后再新值,如以下示例所示:

@JvmStatic
@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(newPadding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }
}

        有时要处理事件绑定时,接口的定义是方法 remove 和 add,而不是 setter,所以可以通过接收旧值的方式来先 remove 然后再 add 新值,如:

布局:

<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

方法和注解:

@JvmStatic
@BindingAdapter("android:onLayoutChange")
fun setOnLayoutChangeListener(
        view: View,
        oldValue: View.OnLayoutChangeListener?,
        newValue: View.OnLayoutChangeListener?
) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        if (oldValue != null) {
            view.removeOnLayoutChangeListener(oldValue)
        }
        if (newValue != null) {
            view.addOnLayoutChangeListener(newValue)
        }
    }
}

4.3. @BindingConversion (类型转换注解)

        在某些情况下,需要在特定类型之间进行自定义转换。例如,View 的 android:background 属性需要 Drawable,但如果通过表达式进行逻辑处理后,指定的 color 值是整数。这样就会发生类型转换错误,例如正常情况下:

<View
   android:background="@color/white"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

但是,如果进行了表达式逻辑处理后:

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

这里我们预期是接收一个Drawable 类型的参数,但此处返回了一个 int 类型参数就会发生类型转换错误。像此类情况,需执行类型的转换,那么就需要使用带有 @BindingConversion 注解的静态方法。

        @BindingConversion 注解的使用跟 @BindingAdapter 类似,也是可以将注解和静态方法定义在任意地方,但要注意的是,@BindingConversion 的方法需要返回值,来看看上述示例中使用 @BindingConversion 如何处理类型的转换:

@JvmStatic
@BindingConversion
fun convertColorToDrawable(color: Int): ColorDrawable {
    return ColorDrawable(color)
}

4.3.1. 同时使用 @BindingConversion 和 @BindingAdapter

        如果同时使用 @BindingConversion@BindingAdapter 的话,@BindingConversion 优先级比 @BindingAdapter 高。示例:

布局:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{`子云心`}" />

注解和方法:

@JvmStatic
@BindingAdapter("android:text")
fun setText(view: TextView, txt: String?) {
    view.text = "BindingAdapter_$txt"
}
@JvmStatic
@BindingConversion
fun bindingConversionTest(txt: String?): String {
    return "BindingConversion_$txt"
}

示例中最终 TextView 展示出来的值是:“BindingAdapter_BindingConversion_子云心”,这是因为存在优先级高低,所以先执行了 @BindingConversion 再执行 @ BindingAdapter的方法。

注意:

    @BindingConversion 一般是在类型转换时才使用,像上述示例中,bindingConversionTest 方法接收一个 String 又返回一个新的 String 其实是没有必要的,因为这仅仅用于演示使用,在实际开发中应该尽量避免这样做,因为这样会使业务复杂化,从而可能导致 @BindingAdapter 中预期接收的值被修改。

    所以在使用的时候,要格外注意。如果不熟悉或者非用不可就不要使用 @BindingConversion了。因为 @BindingConversion 看似很高级但是如果项目业务复杂,驾驭不了还很有可能导致 @BindingAdapter 的值被修改从而增加项目的可维护性。

4.4. @BindingMethods(适用于自定义View的绑定注解)

        @BindingMethods 注解和 @BindingAdapter 注解在功能上类似,都是用于在 XML 布局中绑定自定义属性与 View 的行为,但它们的使用场景有一些不同。

        @BindingAdapter 通常是用来自定义已有的 Android View 控件的行为,而 @BindingMethods 则更适用于自定义View 与 Data Binding 库之间的交互。

    @BindingMethods 注解需将它定义在自定义 View 类的头部,注解内可以有多个BindingMethod 子项。

    假设你有一个自定义的圆形按钮 CircularButton,它有一个设置按钮的颜色的方法和一个设置圆形弧度的方法,可以通过 BindingMethod 来指定这两个方法的映射关系:

@BindingMethods(
    BindingMethod(type = CircularButton::class, attribute = "app:buttonColor", method = "setButtonColor"),
    BindingMethod(type = CircularButton::class, attribute = "app:buttonRadian", method = "setButtonRadian"),
)
class CircularButton : Button {
    constructor(context: Context?) : super(context) {
    }
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
    }
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    }
    fun setButtonColor(@ColorInt color: Int) {
        // TODO...
    }
    fun setButtonRadian(radian: Int) {
        // TODO...
    }
}

在 xml布局中,就可以使用 app:buttonColor 和 app:buttonRadian属性来设置按钮的颜色和弧度:

<com.example.CircularButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:buttonColor="@color/red"
    app:buttonRadian="10" />

5. 双向绑定        

        双向数据绑定使用了@={} 表示法,即比单向多出一个“=”号,它表示可接收属性的数据更改,并同时能监听数据更新。

5.1. 标准控件的绑定

        回顾使用单向数据绑定,它仅可以获取属性值,若要设置其属性值,一般要通过 View 的监听器的方式对其输入变更后的属性进行响应再处理。结合上述 4.1.3可观察对象和 3.4.2监听器绑定的学习,现在来创建一个通过 EditText 输入值后进行数据和界面的更新单向绑定示例:

        再将上面的UserObservable 再进一步改造,增加一个afterNameChanged 方法用于输入后更新 name 的值:

class UserObservable: BaseObservable() {
    @get:Bindable
    var name: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(com.flyme.auto.user.abc.BR.name)
        }
    @get:Bindable
    var age: Int = 0
        set(value) {
            field = value
            notifyPropertyChanged(com.flyme.auto.user.abc.BR.age)
        }
    fun afterNameChanged(text: Editable) {
        name = text.toString()
    }
}

给布局变量赋值:

binding.userObservable = UserObservable().apply {
    name = "子云心"
    age = 18
}

布局:

<data>
    <variable name=" userObservable " type="com.example. UserObservable "/>
</data>
……
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"
    android:afterTextChanged="@{(text) -> userObservable.afterNameChanged(text)}"/>

这个单向绑定示例中,当用户在 EditText 中输入更改后的新值时,由于绑定了值变化监听器,此时 userObservable.afterNameChanged 方法得到调用,方法内对 name 字段进行了设置,又因为 UserObservable 类继承于BaseObservable ,name 字段是可观察字段,所以在 TextView 中是可以立即响应值的变化。

        对于标准控件和常见属性(如 EditText 的 android:text ),其实 DataBinding 库已经内置了支持双向数据绑定,上述示例如果使用双向数据绑定,即通过@={} 表示法,既可获取属性值,也可接收属性值更改同时监听从而更新数据,这样便可简化 EditText 的 afterTextChanged 监听步骤,UserObservable 类中可删除 afterNameChanged 方法,同时布局 View的表达式可以这样变化:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={userObservable.name}" />

有关双向数据绑定的内置支持如下表所示:

特性

绑定适配器

AdapterView

android:selectedItemPosition
android:selection

AdapterViewBindingAdapter

CalendarView

android:date

CalendarViewBindingAdapter

CompoundButton

android:checked

CompoundButtonBindingAdapter

DatePicker

android:year
android:month
android:day

DatePickerBindingAdapter

NumberPicker

android:value

NumberPickerBindingAdapter

RadioButton

android:checkedButton

RadioGroupBindingAdapter

RatingBar

android:rating

RatingBarBindingAdapter

SeekBar

android:progress

SeekBarBindingAdapter

TabHost

android:currentTab

TabHostBindingAdapter

TextView

android:text

TextViewBindingAdapter

TimePicker

android:hour
android:minute

TimePickerBindingAdapter

5.2. @InverseBindingAdapter(双向绑定适配器注解) 

        对于标准控件和常见属性(如 EditText 的 android:text),DataBinding 库已经内置了支持,不需要额外做处理,但是如果对于自定义属性,要想实现数据双向绑定,就要借助于 @InverseBindingAdapter 注解。示例:

修改上述布局 View的表达式:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:customText="@={userObservable.name}" />

在任意代码中增加如下注释和方法

@BindingAdapter("customText")
fun setCustomText(view: EditText, text: String) {
    // 判断新旧值很有必要,否则会导致无限循环
    if (view.text.toString() != text) {
        view.setText(text)
    }
}
@InverseBindingAdapter(attribute = "customText", event = "customTextAttrChanged")
fun getCustomText(view: EditText): String {
    return view.text.toString()
}
@BindingAdapter("customTextAttrChanged")
fun setCustomTextAttrChanged(view: EditText, listener: InverseBindingListener?) {
    if (listener != null) {
        view.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
            override fun afterTextChanged(s: Editable) {
                listener.onChange()
            }
        })
    }
}

示例中,同样的当用户在 EditText 中输入更改后的新值时,TextView 中可以立即响应值的变化。

        上面单向绑定时我们了解到 @BindingAdapter 绑定适配器可支持布局 XML 中的自定义属性进行绑定行为扩展。所以此示例中从逻辑端数据模型到 UI 布局中 View 的数据绑定依然是依赖 @BindingAdapter 绑定适配器来支持。

        而当从 UI 布局中 View 控件文本发生数据变化再同步到逻辑端数据模型中,则是@InverseBindingAdapter 的功劳,它在用户对 EditText 的文本变化时,获取新的值更新到customText 属性。并且它的 event 参数指定了一个值变化监听器,该监听器包含一个 InverseBindingListener 参数。当 EditText 的文本变化后,通知数据绑定系统属性已更改。event 参数可以不指定,如果不指定则默认是自定义属性+ Changed。

5.2.1 注意无限循环情况

        在使用双向数据绑定时,需要特别注意不要引入无限循环。当用户更改属性时,系统会调用使用 @InverseBindingAdapter 注解的方法,并将值分配给后备属性。继而会调用使用 @BindingAdapter 注解的方法,从而触发对使用 @InverseBindingAdapter 注解的方法的另一次调用,依此类推。

        因此,通过在 @BindingAdapter 注解的方法中通过判断新值和旧值是否相等,从而来打破可能引起的无限循环情况。

5.2. @InverseBindingMethods(自定义View的双向绑定注解)

    @InverseBindingMethods 注解跟单向绑定的 @BindingMethods 注解一样,它们都是更适用于自定义View,而 @InverseBindingMethods 就是 @BindingMethods 的双向绑定版本,来看看示例:

自定义EditText:

@BindingMethods(
    BindingMethod(type = CustomEditTextView::class, attribute = "customText", method = "setCustomText")
)
@InverseBindingMethods(
    InverseBindingMethod(type = CustomEditTextView::class, attribute = "customText", method = "getCustomText", event = "customTextAttrChanged")
)
class CustomEditTextView: EditText {
     companion object{
        @JvmStatic
        @BindingAdapter("customTextAttrChanged")
        fun setCustomTextListener(view: EditText, textAttrChanged: InverseBindingListener?) {
            if (textAttrChanged != null) {
                view.addTextChangedListener(object : TextWatcher {
                    override fun beforeTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
                    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
                    override fun afterTextChanged(s: Editable) {
                        textAttrChanged.onChange()
                    }
                })
            }
        }
    }
    constructor(context: Context?) : super(context) {
    }
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
    }
    fun setCustomText(newText: String?) {
        if (text.toString() != newText) {
            setText(newText)
        }
    }
    fun getCustomText():String {
        return text.toString()
    }
}

布局:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{userObservable.name}"/>
<com.example.CustomEditTextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:customText="@={userObservable.name}"/>

示例中,同样当用户在 CustomEditTextView 中输入更改后的新值时,TextView 中可以立即响应值的变化。

        上面单向绑定时我们了解到 @BindingMethods 注解用于支持自定义视图中属性进行绑定行为扩展。所以此示例中从代码到 View 的数据绑定依然是依赖 @BindingMethods 来支持;

        而当从 View 控件文本发生数据变化再同步到代码中,则是 @InverseBindingMethods 的功劳,它跟 @InverseBindingAdapter 一样,也是需要在用户对 EditText 的文本变化时,获取新的值更新到 customText 属性和它的 event 参数指定了一个值变化监听器用于当 EditText 的文本变化后,通知数据绑定系统属性已更改。

注意:

        因为监听器方法必须为静态方法,所以示例中将方法放置在伴生对象中定义并且添加 @JvmStatic 注解。

5.4 @InverseMethod(双向绑定的类型转换)

        从上面的单向绑定学习时了解到,如果 View 中展示在表达式里某一个非 String 类型的字段时,可以直接使用 String.valueOf 进行类型的转换,如:android:text="@{String.valueOf(index + 1)}";如果通过表达式进行逻辑处理后,发生的类型转换,可以使用  @BindingConversion 注解来转换。

        在双向绑定中遇上此情况,就要需要使用 @InverseMethod 来处理双向绑定时的方法逆变换。请看示例:

创建一个Converter类:

object Converter {
    @JvmStatic
    @InverseMethod("stringToInt")
    fun intToString(value: Int): String {
        return value.toString()
    }
    @JvmStatic
    fun stringToInt(value: String): Int {
        return try {
            value.toInt()
        } catch (e: NumberFormatException) {
            0
        }
    }
}

布局:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={Converter.intToString(user.age)}"
    android:inputType="number" />

同样的 @InverseMethod 注解也必须为静态方法,以及它接收另一个反向转换方法名称作为参数。

6. 总结

        Data binding 是官方推荐的视图数据绑定方案,它可以减少了手动编写代码来实时同步 UI 和数据的工作量,使开发更加高效,它的自动同步机制也减少了人为同步时容易产生的错误情况,而且可以更容易地维护和扩展代码,因为 UI 逻辑和数据逻辑被明确分离。

        更多详细的 View binding 介绍可前往 Android 开发者官网:https://developer.android.com/topic/libraries/data-binding?hl=zh-cn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1863475.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

L59---101.对称二叉树(广搜)---Java版

1.题目描述 2.思路和知识点 &#xff08;1)根节点为空&#xff1a; 如果根节点为空&#xff0c;树是对称的。 (2)递归检查&#xff1a; isMirror 方法递归检查两个子树是否是镜像对称的。 (3)辅助函数 isMirror&#xff1a; 1)如果两个节点都为空&#xff0c;它们是镜像对称的…

php composer 报错

引用文章&#xff1a; Composer设置国内镜像_composer 国内源-CSDN博客 php composer.phar require --prefer-dist yiidoc/yii2-redactor "*" A connection timeout was encountered. If you intend to run Composer without connecting to the internet, run the …

day49---数据结构与算法(四)

三. 基础算法 3.1 查找概述 查找算法是一种在数据集中寻找特定数据项的方法。通常&#xff0c;数据集是在计算机程序中存储的&#xff0c;例如数组、链表或散列表。在编写程序时&#xff0c;查找算法是非常重要的&#xff0c;它有助于快速找到所需的数据。在本文中&#xff0…

Linux系统安装Lua语言及Lua外部库

安装Lua Lua语言是一种轻量级、高效且可扩展的脚本语言&#xff0c;具有简洁易学的语法和占用资源少的特点。它支持动态类型&#xff0c;提供了丰富的表达式和运算符&#xff0c;同时具备自动垃圾回收机制和跨平台性。Lua语言易于嵌入到其他应用程序中&#xff0c;并可与其他语…

高性能并行计算华为云实验五:PageRank算法实验

目录 一、实验目的 二、实验说明 三、实验过程 3.1 创建PageRank源码 3.2 makefile的创建和编译 3.3 主机配置文件建立与运行监测 四、实验结果与分析 4.1 采用默认的节点数量及迭代次数进行测试 4.2 分析并行化下节点数量与耗时的变化规律 4.3 分析迭代次数与耗时的变…

2024广东省职业技能大赛云计算赛项实战——集群部署GitLab Agent

集群部署GitLab Agent 前言 题目如下&#xff1a; 部署GitLab Agent 将Kubernetes集群添加到demo-2048项目中&#xff0c;并命名为kubernetes-agent&#xff0c;项目命名空间选择gitlab-ci。 说是部署GitLab Agent,但据我了解&#xff0c;Agent就是Runner&#xff0c;看题目…

5.How Fast Should You Be When Learning?(你应该用多快的速度学习? (一))

Normally when I talk about learing quickly, I’m using speed as a synonym for efficiency.Use more effective methods and you’ll learn more in less time.All else being equal, that means you’re learing faster. 通常我在谈到快速学习时&#xff0c;是把“速度&qu…

【神经网络】神经元的基本结构和训练过程

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步&#xff01; 神经元的基本结构和训练过程 …

Redis数据库(三):Redis数据库三种特殊数据类型

除了上一篇博客讲的五种基本数据类型外&#xff0c;Redis还有三种特殊的数据类型&#xff0c;它们有着不同的应用场景&#xff0c;这一篇博客&#xff0c;我们来学习它。 目录 一、geospatial 地理空间 1.1 添加地理位置 1.2 返回给定名称的纬度和经度 1.3 返回两个给定位…

小柴冲刺嵌入式系统设计师系列总目录

工作两年 逐渐意识到基础知识的重要性✌️ 意识到掌握了这个证书好像就已经掌握了80%工作中用到的知识了。剩下的就在工作的实战中学习 来和小柴一起冲刺软考吧&#xff01;加油&#x1f61c; 【小柴冲刺软考中级嵌入式系统设计师系列】总目录 前言 专栏目标&#xff1a;冲刺…

ros2_control 使用教程

系列文章目录 前言 0.1 欢迎阅读 ros2_control 文档&#xff01; ros2_control 是一个使用&#xff08;ROS 2&#xff09;对机器人进行&#xff08;实时&#xff09;控制的框架。其软件包是对 ROS&#xff08;机器人操作系统&#xff09;中使用的 ros_control 软件包的重写。r…

NetSuite CSV导入类型与记录类型梳理

最近有用户问到我们的一个问题是&#xff0c;哪些数据可以使用CSV导入&#xff0c;哪些数据不能使用CSV导入&#xff0c;干脆咱们就整理出来可使用CSV导入功能的类型和记录类型&#xff0c;供大家直接参考&#xff5e; 但是有一些内容或多或少由于每个企业的环境不一样而有所不…

jenkins环境搭建--关于jenkins在Ubuntu下的安装篇(一)

在ubuntu下使用命令进行下载安装包&#xff1a; 关于jenkins的安装有多种&#xff0c;可以借助docker容器进行安装&#xff0c;也可以通过传统方法手动一步步的进行安装&#xff0c;以下介绍手动一步步的安装方法&#xff0c;后续我们将解释关于jenkins的相关配置以及实战使用…

mongodb 查询语句学习笔记

基础查询 正则查询 {status: A,$or: [{ qty: { $lt: 30 } }, { item: { $regex: ^p } }] }AND 查询 { "size.h": { $lt: 15 }, "size.uom": "in", status: "D" }OR 查询 { $or: [ { status: "A" }, { qty: { $lt: 30 } …

万界星空科技自动化运维管理---设备管理

在信息化管理体系建设中&#xff0c;设备管理系统被看作是重中之重。因为设备是工厂生产中的主体、生命线&#xff0c;随着科学技术的不断发展、智能制造的产业升级&#xff0c;生产设备日益智能化、自动化&#xff0c;设备在现代工业生产中的作用和影响也随之增大&#xff0c;…

智能体——父亲兴趣爱好助手

&#x1f3bc;个人主页&#xff1a;【Y小夜】 &#x1f60e;作者简介&#xff1a;一位双非学校的大二学生&#xff0c;编程爱好者&#xff0c; 专注于基础和实战分享&#xff0c;欢迎私信咨询&#xff01; &#x1f386;入门专栏&#xff1a;&#x1f387;【MySQL&#xff0…

Sectigo或RapidSSL DV通配符SSL证书哪个性价比更高?

在当前的网络安全领域&#xff0c;选择一款合适的SSL证书对于保护网站和用户数据至关重要。Sectigo和RapidSSL作为市场上知名的SSL证书提供商&#xff0c;以其高性价比和快速的服务响应而受到市场的青睐。本文将对Sectigo和RapidSSL DV通配符证书进行深入对比&#xff0c;帮助用…

java设计模式(四)原型模式(Prototype Pattern)

1、模式介绍&#xff1a; 原型模式&#xff08;Prototype Pattern&#xff09;是一种创建型设计模式&#xff0c;它允许对象在创建新实例时通过复制现有实例而不是通过实例化新对象来完成。这样做可以避免耗费大量的资源和时间来初始化对象。原型模式涉及一个被复制的原型对象…

【机器学习】在【R语言】中的应用:结合【PostgreSQL数据库】的【金融行业信用评分模型】构建

目录 1.数据库和数据集的选择 1.准备工作 2.PostgreSQL安装与配置 3.R和RStudio安装与配置 2.数据导入和预处理 1.连接数据库并导入数据 1.连接数据库 2.数据检查和清洗 1.数据标准化 2.拆分训练集和测试集 3.特征工程 1.生成新特征 2.特征选择 4.模型训练和评估…

【浦语开源】深入探索:大模型全链路开源组件 InternLM Lagent,打造灵笔Demo实战指南

一、准备工作&#xff1a; 1、环境配置&#xff1a; pip、conda换源&#xff1a; pip临时换源&#xff1a; pip install -i https://mirrors.cernet.edu.cn/pypi/web/simple some-package# 这里的“https://mirrors.cernet.edu.cn/pypi/web/simple”是所换的源&#xff0c;…