Android Jetpack-Databinding

355 阅读4分钟

一、概述

DataBinding可以通过在XML中定义一些逻辑,来处理一部分数据与UI的交互,来达到进一步精简视图中交互逻辑的目的。

DataBinding处于一种特别尴尬的地位,findViewById的功能已经被官方推荐用ViewBinding取代,XML已经慢慢开始被官方推出的 新组件Compose取代,DataBinding渐渐已经没有了用武之地,但是现在很多框架还是在用。

二、配置

1、gradle配置

在模块的build.gradle文件中添加如下配置

plugins {
    ...
    id 'kotlin-kapt'  //插件,用到注解时添加
}

android {
    ...
    buildFeatures {
        dataBinding true //启用dataBinding
    }  
}

2、xml配置

先看xml的基础样式

<?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>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        ...
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

3、适配JSK11

如果运行发现报错A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution,可能需要适配jdk11,在镜像站下载jdk11,并在Android Studio中配置:

image.png

image.png

三、标签说明

1、<layout>标签

<layout>标签是必须的,只有被该标签包裹的布局才能被Databinding识别,所以<layout>是xml文件的根标签。该标签下有且只能拥有二个子标签,一个是<data>标签,用来包裹具体与xml中控件交互的数据或者事件;另一个标签就是布局的实际内容。

2、<data>标签

<data>标签用于声明用到的变量以及变量类型。创建一个简单的UserInfo类:

data class UserInfo(var name: String?, var age: Int) {
    override fun toString(): String {
        return "UserInfo(name='$name', age=$age)"
    }
}

在data标签里声明要使用到的变量名和类的全路径,如下:

<data>
    <variable
        name="UserInfo"
        type="com.abc.testlifecycle.bean.UserInfo" />
</data>

如果多个不同UserInfo类要在XML中被引用,那么可以用import标签导入来简化写法,如下:

<data>
    <import type="com.abc.testlifecycle.bean.UserInfo"/>
    <variable
        name="UserInfo"
        type="UserInfo" />

    <variable
        name="UserInfo2"
        type="UserInfo" />
</data>

如果存在import的类名相同的情况,可以使用alias指定别名:

<data>
    <import type="com.abc.testlifecycle.bean.UserInfo"/>
    <import
        alias="ReqUserInfo"
        type="com.abc.testlifecycle.req.UserInfo"/>
    
    <variable
        name="UserInfo"
        type="UserInfo" />

    <variable
        name="ReqUserInfo"
        type="ReqUserInfo" />
</data>

每个<data>标签都会为XML文件生成一个绑定类,名称是根据布局文件名生成首字母大写以驼峰命名法来命名的文件,例如activity_main.xml文件生成的绑定类为ActivityMainBinding。如果想修改默认生成的绑定类名,可以在<data>标签中自定义,如下:

<data class="MainActivityBinding">
   ...
</data>

四、绑定视图

对于普通的布局xml视图,我们可以通过View.inflate()LayoutInflater.from(this).inflate()等方法将xml转化为可以访问View的绑定操作类,DataBinding就给我们提供了这么一套方法,由DataBindingUtil提供,inflate原理和以前是一样的:

//ActivityMainBinding为自动生成的绑定类
val binding = DataBindingUtil.inflate<ActivityMainBinding>(
    layoutInflater,R.layout.activity_main,parent,true
)

如果是给Activity设置视图,可以这样写:

    override fun onCreate(savedInstanceState:Bundle{
        super.onCreate(savedInstanceState) 
        //ActivityMainBinding为自动生成的绑定类
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main) 
    }

如果是给Fragment设置视图,可以这样写:

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View? {
        var binding = DataBindingUtil.inflate<FragmentCustomBinding>(inflater,
            R.layout.fragment_custom,
            container,
            false)
        return binding.root
    }
}

还有一种简便的方法就是通过绑定类的静态方法进行inflate:

val binding = ActivityMainBinding.inflate(layoutInflater,parent ,true)

除上述方法外,DataBindingUtil类还有一个bind方法,该方法可以传入一个View得到对应的绑定类:

var binding = DataBindingUtil.bind<FragmentCustomBinding>(view)

五、布局文件中的一些常用用法

1、引入对象的数据

<?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>
        <variable
            name="userInfo"
            type="com.abc.testlifecycle.bean.UserInfo" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

       
        <TextView   //字符串拼接加设置默认值,userInfo为上面data中的对象userInfo
            android:id="@+id/tv_start"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`姓名:`+userInfo.name,default=`姓名:张三`}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
    
</layout>

在MainActivity中设置值

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding =
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    binding.userInfo = UserInfo("李四", 20, "134 5677 7788")
}

2、字符串转换和导包引入更多自定义代码

如果我们想将一个int值设置给TextView应该如何处理?

android:text="@{String.valueOf(userInfo.age),default=`18`}"

里面可以写代码,但是需要导入我们需要的类,比如定义了一个校验手机号的伪代码:

object  CheckUtils {
    fun checkPhone(phone:String):String = phone
}

data标签内导入

<import type="com.abc.testlifecycle.utils.CheckUtils" />

使用

android:text="@{CheckUtils.INSTANCE.checkPhone(userInfo.phone),default=`123 1245 1234`}"

3、三目运算符

android:text="@{userInfo.age>18?`成年`:`未成年`,default=`成年`}"

空合并运算符 ?? 会取第一个不为 null 的值作为返回值,比如在界面上设置用户名时如果用户名为空,就设置用户手机号为用户名:

android:text="@{userInfo.name??userInfo.phone,default=`未设置用户名`}"

你甚至可以继续追加

android:text="@{userInfo.name??userInfo.phone??`未设置用户名`,default=`用户名`}"
binding.userInfo = UserInfo(null, 20,null)

结果为

image.png

4、资源引用和属性控制

引用字符串

<string name="content">%1$s非%1$s,%2$s非%2$s</string>
android:text="@{@string/content(`花`,`雾`)}"

结果

image.png

控制控件的可见不可见

<import type="android.view.View" />
android:visibility="@{userInfo.isVip?View.VISIBLE:View.GONE}"

5、加载图片

DataBinding 提供了 @BindingAdapter 这个注解用于支持自定义属性,或者是修改原有属性。注解值可以是已有的 xml 属性,例如 android:src、android:text等,也可以自定义属性然后在 xml 中使用。

定义加载图片的方法


object ImageLoaderUtils {

    //定义自定义属性的名称
    @BindingAdapter("image_url")
    //方法静态
    @JvmStatic
    fun loadImage(image: ImageView, url: String) {
        //简单的Glide加载图片的方法
        Glide.with(image.context).load(url).into(image)
    }
}

使用自定义属性

app:image_url="@{userInfo.photo}"

调用

var url = "https://img.wxcha.com/m00/7c/be/9ddde1a370bca73daa50a01aeabdfb10.jpg"
binding.userInfo = UserInfo(null, 20, null, true, url)

结果

image.png

@BindingAdapter可以重新定义或扩展Android控件的属性,比如我们在给TextView设置int值时会报错,可以扩展使它不报错:

//android:text是一个int值
android:text="@{userInfo.age}"

扩展

@BindingAdapter("android:text")   //扩展或者重新定义android:text
@JvmStatic
fun setText(textview: TextView, value: Int) {
    textview.text = "$value"   //int转化为字符串
}

6、Array、List、Set、Map

dataBinding 也支持在布局文件中使用数组、Lsit、Set 和 Map,且在布局文件中都可以通过 list[index] 的形式来获取元素。

而为了和 variable 标签的尖括号区分开,在声明 Lsit< String > 之类的数据类型时,需要使用尖括号的转义字符<&lt;>&gt;。Map类型可以这样写 @{map[key]} 可替换为 @{map.key}

<data>
    <import type="java.util.List" />
    <import type="com.abc.testlifecycle.bean.UserInfo" />

    <variable
        name="userInfoList"
        type="List&lt;UserInfo&gt;" />
</data>
android:text="@{userInfoList[0].name}"

7、引用同一布局的控件

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

六、可观察字段

  • ObservableBoolean
  • ObservableByte
  • ObservableChar
  • ObservableShort
  • ObservableInt
  • ObservableLong
  • ObservableFloat
  • ObservableDouble
  • ObservableParcelable 可观察字段是具有单个字段的自包含可观察对象。原语版本避免在访问操作期间封箱和开箱。如需使用此机制,请采用 Java 编程语言创建 public final 属性,或在 Kotlin 中创建只读属性,如以下示例所示:
    class User {
        val firstName = ObservableField<String>()
        val lastName = ObservableField<String>()
        val age = ObservableInt()
    }

七、可观察集合

某些应用使用动态结构来保存数据。可观察集合允许使用键访问这些结构。以List可观察集合有ObservableList(接口),ArrayList(实现类),ObservableArrayList(实现类),ObservableMap(接口),ArrayMap(实现类),ObservableArrayMap(实现类)等。

<data>
    <import type="androidx.databinding.ObservableList" />
    <import type="com.abc.testlifecycle.bean.UserInfo" />
    
    <variable
        name="userInfoList"
        type="ObservableList&lt;UserInfo&gt;" />
</data>

八、可观察对象

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

Observable 接口具有添加和移除监听器的机制,但何时发送通知必须由您决定。为便于开发,数据绑定库提供了用于实现监听器注册机制的 BaseObservable类。实现 BaseObservable 的数据类负责在属性更改时发出通知。具体操作过程是向 getter 分配 Bindable注释,然后在 setter 中调用 notifyPropertyChanged() 方法,如以下示例所示:

    class User : BaseObservable() {

        @get:Bindable
        var firstName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.firstName)
            }

        @get:Bindable
        var lastName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.lastName)
            }
    }
    

九、LiveData和ViewModel关联使用

要将 LiveData 对象与绑定类一起使用,您需要指定生命周期所有者来定义 LiveData 对象的范围。以下示例在绑定类实例化后将 Activity 指定为生命周期所有者:

    class ViewModelActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            // Inflate view and obtain an instance of the binding class.
            val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)

            // Specify the current activity as the lifecycle owner.
            binding.setLifecycleOwner(this)
        }
    }

使用 ViewModel组件来将数据绑定到布局。在 ViewModel 组件中,您可以使用 LiveData 对象转换数据或合并多个数据源。以下示例展示了如何在 ViewModel 中转换数据:

    class ScheduleViewModel : ViewModel() {
        val userName: LiveData

        init {
            val result = Repository.userName
            userName = Transformations.map(result) { result -> result.value }
        }
    }
    

要将 ViewModel 组件与数据绑定库一起使用,必须实例化从 ViewModel 类继承而来的组件,获取绑定类的实例,并将您的 ViewModel 组件分配给绑定类中的属性。以下示例展示了如何将组件与库结合使用:

    class ViewModelActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            // Obtain the ViewModel component.
            val userModel: UserModel by viewModels()

            // Inflate view and obtain an instance of the binding class.
            val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)

            // Assign the component to a property in the binding class.
            binding.viewmodel = userModel
        }
    }
    

在您的布局中,使用绑定表达式将 ViewModel 组件的属性和方法分配给对应的视图,如以下示例所示:

<CheckBox
        android:id="@+id/rememberMeCheckBox"
        android:checked="@{viewmodel.rememberMe}"
        android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />
    

十、双向绑定

@={} 表示法(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。

一个简单的双向绑定的例子:

<data>

    <import type="com.abc.testlifecycle.bean.UserInfo" />

    <variable
        name="userInfo"
        type="UserInfo" />
</data>
<EditText
    android:id="@+id/et_start"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={userInfo.name}"   //将userInfo.name设置给edittext,同时输入文本改变userInfo.name
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

将userInfo.name设置给edittext,同时输入文本改变userInfo.name

var userInfo = UserInfo("zhangsan", 20, null, false, url)
 binding.userInfo= userInfo

 binding.etStart.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?) {
        Log.d(TAG,"打印name=${userInfo.name}")
     }

 })

参考了以下博客,表示感谢:

DataBinding其一 作者:余半生

Android Jetpack(2):DataBinding的基本使用

--个人学习笔记