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

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><LinearLayoutandroid: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. 还可以导入某一个类,然后在表达式中使用该类的静态方法。

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

<TextViewandroid:text="@{user.name}"android:layout_width="wrap_content"android:layout_height="wrap_content"android:visibility="@{user.age >=18 ? View.VISIBLE : View.GONE}"/><TextViewandroid: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><LinearLayoutandroid: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,示例:

<EditTextandroid:id="@+id/example_text"android:layout_height="wrap_content"android:layout_width="match_parent"/>
<TextViewandroid: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 属性,而不是默认表达式值。示例:

<LinearLayoutxmlns: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><LinearLayoutandroid: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: ActivityMainBindingoverride 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>
...
<TextViewandroid: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>
...<TextViewandroid: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:Bindablevar name: String = ""set(value) {field = valuenotifyPropertyChanged(com.example.BR.name)}@get:Bindablevar age: Int = 0set(value) {field = valuenotifyPropertyChanged(com.example.BR.age)}
}

给布局变量赋值:

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

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

<data><variable name=" userObservable " type="com.example. UserObservable "/>
</data>
...<TextViewandroid: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. 已存在属性

布局:

<TextViewandroid: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. 自定义属性

布局:

<ImageViewandroid: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. 同时多个自定义属性

布局:

<ImageViewandroid: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 值是整数。这样就会发生类型转换错误,例如正常情况下:

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

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

<Viewandroid: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 高。示例:

布局:

<TextViewandroid: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.CircularButtonandroid: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:Bindablevar name: String = ""set(value) {field = valuenotifyPropertyChanged(com.flyme.auto.user.abc.BR.name)}@get:Bindablevar age: Int = 0set(value) {field = valuenotifyPropertyChanged(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>
……
<TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="@{userObservable.name}"/>
<EditTextandroid: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的表达式可以这样变化:

<TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="@{userObservable.name}"/>
<EditTextandroid: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的表达式:

<TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="@{userObservable.name}"/>
<EditTextandroid: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()}
}

布局:

<TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="@{userObservable.name}"/>
<com.example.CustomEditTextViewandroid: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()}@JvmStaticfun stringToInt(value: String): Int {return try {value.toInt()} catch (e: NumberFormatException) {0}}
}

布局:

<EditTextandroid: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.mzph.cn/web/36141.shtml

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

相关文章

【深度学习】服务器炼丹代码配置、Python使用指定gpu显卡运行代码

【显卡】服务器炼丹代码配置 写在最前面一、查看哪几块显卡能用二、使用指定gpu运行代码1、指定使用GPU0运行脚本&#xff08;默认是第一张显卡, 0代表第一张显卡的id,其他的以此类推&#xff09;2、指定使用多张显卡运行脚本 三、如何使用1、单块显卡使用2、多GPU训练使用Data…

昇思25天学习打卡营第3天|数据集全攻略:加载、操作与自定义

导入数据集相关库和类 首先&#xff0c;导入了 NumPy 库&#xff0c;并将其简称为 np 。要知道&#xff0c;NumPy 乃是用于科学计算的关键库&#xff0c;作用非凡。接着&#xff0c;从 mindspore.dataset 当中导入了 vision 模块。此外&#xff0c;还从 mindspore.dataset 里引…

小阿轩yx-Nginx 网站服务

小阿轩yx-Nginx 网站服务 由俄罗斯的 lgor Sysoev 开发其稳定、高效的特性逐渐被越来越多的用户认可 Nginx 服务基础 Nginx (发音为[engine x])专为性能优化而开发 最知名的优点 稳定性低系统资源消耗以及对 HTTP 并发连接的高处理能力(单台物理服务器可支持 30000~50000个…

抗击.michevol勒索病毒:保障数据安全的新策略

导言&#xff1a; 在今天高度互联的数字化环境中&#xff0c;数据安全面临着越来越复杂和普遍的威胁&#xff0c;勒索病毒如.michevol已成为了用户和企业普遍面临的风险。本文91数据恢复将探讨.michevol勒索病毒的特点、感染方式以及创新的防御策略&#xff0c;旨在帮助读者更…

车载双向认证框架设计

最近工作需要&#xff0c;手写了一个双向认证库&#xff0c;可以用在Java、Android上&#xff0c;不限于PC/手机、车载平台。首先我们来看看双向认证的原理机框架设计思路&#xff0c;最后会给出下载链接大家可以体验或者源码参考。 因为可以和FlexNet网络库&#xff08;参考我…

mac安装navicate

1.下载好之后点击安装包安装 2.一般情况下会提示安全性什么的,点击允许即可,然后会出现如下界面,点击安装即可. 3.点击打开 4.然后出现如下界面,点击Enter 5.将安装包拖入即可. 6.等待安装即可 7.安装完成后会在启动台看到Navicat16 的图标 8.然后打开软件界面如下:

访问外网的安全保障——反向沙箱

反向沙箱作为一种网络安全技术&#xff0c;其核心理念在于通过构建一个隔离且受控的环境&#xff0c;来有效阻止潜在的网络威胁对真实系统的影响。在当今日益复杂的网络环境中&#xff0c;如何借助反向沙箱实现安全上网&#xff0c;已成为众多用户关注的焦点。 随着信息化的发…

树莓派Pico

树莓派Pico是树莓派基金会推出的一款基于RP2040微控制器的微型计算机板&#xff0c;它是专为需要高性能微控制器的应用场景设计的&#xff0c;特别适合于需要实时控制、低功耗和小型化解决方案的项目。以下是树莓派Pico的详细介绍&#xff1a; ### 核心特点&#xff1a; - **基…

统信系统实战(2):安装redis

在系统中未发现redis,需要安装。 网上资料上说需要去redis官网下载,但是发现不管是github账号还是自己注册的sso账号,都各种提示有问题。 继续找资料,发现可以直接通过下载链接下载,指令如下: wget http://download.redis.io/releases/redis-6.0.6.tar.gz 成功下载,…

仓颉开发入门初体验

作者&#xff1a;黄林晴 顺便吆喝一声&#xff0c;如果你计算机、软件工程、电子等相关专业本科及以上学历&#xff0c;欢迎来共事。前端/后端/测试均可投&#xff0c;技术大厂。 前言 在刚刚召开的华为开发者大会&#xff08;HDC 2024&#xff09;上&#xff0c;华为内部研…

如何在Java中使用Levenshtein距离实现字符串相似度匹配

在许多应用中&#xff0c;我们需要根据用户输入的问题找到最匹配的已知问题。Levenshtein距离&#xff08;编辑距离&#xff09;是一个强大的工具&#xff0c;可以帮助我们衡量两个字符串之间的差异&#xff0c;并进一步计算它们的相似度。本文将使用一个具体的例子来展示如何在…

从架构设计的角度分析ios自带网络库和AFNetworking

总结&#xff08;先说明文章分析出的一些‘认知’&#xff09; 从本文中&#xff0c;我们可以总结出一些框架设计上的“认知”&#xff1a; 对于通用的常规配置信息方面的设计&#xff0c;我们可以通过定义一个“类似于NSURLSessionConfiguration、NSURLRequest”的类来完成设…

【算法专题--栈】后缀表达式求值 -- 高频面试题(图文详解,小白一看就会!!)

目录 一、前言 二、题目描述 三、解题方法 ⭐解题思路 ⭐案例图解 四、总结与提炼 五、共勉 一、前言 后缀表达式求值 这道题&#xff0c;可以说是--栈专题--&#xff0c;最经典的一道题&#xff0c;也是在面试中频率最高的一道题目&#xff0c;通常在面试中&…

什么是ArchiMate?有优缺点和运用场景?

一、什么是ArchiMate? ArchiMate是一种由The Open Group发布的企业级标准&#xff0c;它是一种整合多种架构的可视化业务分析模型语言&#xff0c;也属于架构描述语言&#xff08;ADL&#xff09;。ArchiMate主要从业务、应用和技术三个层次&#xff08;Layer&#xff09;&…

QT在visual studio环境打开控制台窗口

明确需求 在VS环境中开发QT应用&#xff0c;有时遇到BUG想看日志&#xff0c;但是默认VS环境没有显示控制台窗口可看日志。 解决方法 对工程名单击右键。 点击属性&#xff0c;在打开界面按照如下图操作。 设置完成后弹出的控制台窗口如下图。

[Cloud Networking] VLAN

1 为什么需要 VLAN(Virtual Local Area Network) VLAN是一个逻辑网络&#xff0c;VLAN将设备/用户进行逻辑分组&#xff0c;VLAN需要在Switch上创建。为什么需要这样呢&#xff1f;为何不能所有设备都在同一个网络&#xff1f; 如下网络&#xff0c;如果设备过多&#xff0c;…

【日记】怎么上了一天班饭都没得吃(659 字)

正文 今天算是混得最惨的一天了。 下午开始&#xff0c;柜面主管出差&#xff0c;她找了个代班&#xff0c;好家伙&#xff0c;代班直接不下来&#xff0c;于是整个营业室就只有我一个人了。所有客户逮着我一个人薅。我才下来一个月啊…… 明天她还不回来&#xff0c;要下周一&…

【UE5.3】笔记6-第一个简单小游戏

打砖块小游戏&#xff1a; 1、制造一面砖块组成的墙 在关卡中放置一个cube&#xff0c;放这地面上&#xff0c;将其转换成蓝图类,改名BP_Cube&#xff0c;更换砖块的贴图&#xff0c;按住alt键进行拷贝&#xff0c;堆出一面墙&#xff0c;复制出来的会很多&#xff0c;全选移动…

报餐小程序可以运用在饭堂的哪方面

随着科技的快速发展&#xff0c;智能化、信息化的管理方式逐渐渗透到我们日常生活的方方面面。在饭堂管理中&#xff0c;报餐小程序的应用为传统的餐饮管理方式带来了革命性的变革。本文将探讨报餐小程序在饭堂管理中的应用及其带来的优势。 一、报餐小程序的基本功能 报餐小程…

数据资产管理的艺术:构建智能化、精细化的数据资产管理体系,从数据整合、分析到决策支持,为企业提供一站式的数据资产解决方案,助力企业把握数字时代的新机遇

一、引言 在数字化浪潮席卷全球的今天&#xff0c;数据已经成为企业最重要的资产之一。如何高效、安全地管理这些海量数据&#xff0c;从中提取有价值的信息&#xff0c;并将其转化为决策支持&#xff0c;是每个企业都必须面对的挑战。本文将探讨数据资产管理的艺术&#xff0…