Bootstrap

Android Jetpack(2):DataBinding的使用

Android DataBinding 从入门到进阶

DataBinding 介绍

DataBinding是什么?

DataBinding 是谷歌官方发布的一个框架,顾名思义即为数据绑定,是 MVVM 模式在 Android 上的一种实现,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。

DataBinding将布局xml中将控件和数据进行绑定,使数据变化可以驱动控件改变,控件改变可以驱动数据改变。

减少了Activity中对控件的初始化、设置监听、显示数据等操作。

使用databinding你就不需要使用findviewbyid()、setText()等。

MVVM 相对于 MVP,其实就是将 Presenter 层替换成了 ViewModel 层。DataBinding 能够省去我们一直以来的 findViewById() 步骤,大量减少 Activity 内的代码,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常。

ViewBinding

视图绑定组件

配置ViewBinding

android {
        ...
        viewBinding {
            enabled = true
        }
    }

通过视图绑定功能,您可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。在大多数情况下,视图绑定会替代 findViewById。

区别

与findViewById的区别:空安全和类型安全,不存在因引用了一个错误的id而导致的空指针异常或者类型转换异常。

与databinding的区别:databinding仅处理使用 layout代码创建的数据绑定布局;ViewBinding不支持布局变量或布局表达式,因此它不能用于在xml中将布局与数据绑定。

与Android Kotlin Extensions的区别:在使用上,后者简单粗暴,直接id进行访问,而View Binding需要创建绑定类的实例;后者有一些不友好的地方,比如相同的id存在于多个xml,容易导错包,如果包导错了,会有可能别的View用错id导致空指针,而View Binding显然不会有这种情况。

2020年11月11日更新:Android Stuidio 4.1及以上版本,新创建的项目已默认移除kotlin-android-extensions插件

DataBinding基本使用

启用 DataBinding

  • 启用 DataBinding 的方法是在对应 Model 的 build.gradle 文件里加入以下代码,同步后就能引入对DataBinding 的支持。
apply plugin: 'kotlin-kapt'//必须

android {
 dataBinding {
    enabled = true
 }
	  //AS 4.1之后
  bindingFeature{
		dataBinding = true
		// for view binding :
		// viewBinding = true
  }
  
}

就是这么简单,一个简单的databinding配置之后,就可以开始使用数据绑定了。

生成DataBinding布局

我们来看看布局文件该怎么写,首先布局文件不再是以传统的某一个容器作为根节点,而是使用layout作为根节点,在layout节点中我们可以通过data节点来引入我们要使用的数据源。

启用 DataBinding 后,打开原有的布局文件,选中根布局的 根布局,按住 Alt + 回车键,点击 “Convert to data binding layout”,就可以生成 DataBinding 需要的布局规则。

在这里插入图片描述

  • 转换后的内容为:
<?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">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            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>

和原始布局的区别在于多出了一个 layout 标签将原布局包裹了起来,data 标签用于声明要用到的变量以及变量类型,要实现 MVVM 的 ViewModel 就需要把数据(Model)与 UI(View)进行绑定,data 标签的作用就像一个桥梁搭建了 View 和 Model 之间的通道。

设置基本类型数据

在data中定义的variable节点,name属性表示变量的名称,type表示这个变量的类型,实例就是我们实体类的类的全路径。

<?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="userName"
            type="String" />

        <variable
            name="age"
            type="Integer" />

    </data>

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

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{userName}"
            android:textColor="#f00"
            android:textSize="20sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
		
		<!--int 要转为string,使用拼接,或者valueOf,toString都可以。-->
        <TextView
            android:id="@+id/tv_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(age)}"
            android:textColor="#f00"
            android:textSize="20sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_name" />


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

MainActivity

class MainActivity : AppCompatActivity() {
	
	private val binding: ActivityMainBinding by lazy {
        DataBindingUtil.setContentView(this, R.layout.activity_main)
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main)

       // val binding =DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        binding.userName = "xyh"
        binding.age = 20
    }
}

设置对象数据

  • 定义一个实体类

要使用数据绑定,我们得首先创建一个实体类:

data class User(val name:String,val age:Int,val phoneNum:String)
  • Model 与 布局文件关联,设置数据

在data中定义的variable节点,name属性表示变量的名称,type表示这个变量的类型,实例就是我们实体类的类的全路径。

这里声明了一个 User 类型的变量 user,我们要做的就是使这个变量与TextView 控件挂钩,通过设置 user的变量值同时使 TextView 显示相应的文本。

通过 @{user.name} 使 TextView 引用到相关的变量,DataBinding 会将之映射到相应的 getter 方法。

<?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="user"
            type="com.example.jetpack.bean.User" />
    </data>

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

        <TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{`名字`+user.name}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <!--注意:这里age是int类型,必须转化为String,否则会运行时异常-->
        <TextView
            android:id="@+id/tvAge"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{String.valueOf(user.age)}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvName" />

        <TextView
            android:id="@+id/tvPhoneNum"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{user.phoneNum==null?user.phoneNum:`17817318877`}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvAge" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
  • Activity 中通过 DataBindingUtil 设置布局文件

Activity 中通过 DataBindingUtil 设置布局文件,省略原先 Activity 的 setContentView() 方法。

每个数据绑定布局文件都会生成一个绑定类,ViewDataBinding 的实例名是根据布局文件名来生成,将之改为首字母大写的驼峰命名法来命名,并省略布局文件名包含的下划线。

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

        val binding=DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)

        binding.user= User("赵丽颖",20,"17817318859")
    }
}

一个简单的dataBinding案例就已经完成。

代码中获取布局文件中的控件

使用binding对象可获取布局文件中的各个对象,根据控件设置的id来获取。如

        <Button
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="按钮"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />
binding.btn.setOnClickListener {
       Toast.makeText(this,"点击了按钮",Toast.LENGTH_SHORT).show()
}

DataBinding在布局文件中的一些常用用法

在data中定义的variable节点,name属性表示变量的名称,type表示这个变量的类型,实例就是我们实体类的类的全路径。

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

1. 自定义 ViewDataBinding 的实例名

可以通过如下方式自定义 ViewDataBinding 的实例名

<data class="CustomBinding">

</data>
   private val binding: CustomBinding by lazy {
        DataBindingUtil.setContentView(this, R.layout.activity_main)
    }

2. import

如果 User 类型要多处用到,也可以直接将之 import 进来,这样就不用每次都指明整个包名路径了,而 java.lang.* 包中的类会被自动导入,所以可以直接使用

  <data>
        
        <import type="com.example.jetpack.bean.User"/>

        <variable
            name="user"
            type="User" />
    </data>

可以导入java或kotlin文件中的系统类,比如 import 集合 list:

<import type="java.util.List"/>

3. alias

先使用import节点将User导入,然后直接使用即可。但是如果这样的话又会有另外一个问题,假如我有两个类都是User,这两个UserBean分属于不同的包中,又该如何?这时候就要用到alias了。

在import节点中还有一个属性叫做alias,这个属性表示我可以给该类取一个别名,我给User这个实体类取一个别名叫做Lenve,这样我就可以在variable节点中直接写Lenve了。

    <data>
        
        <import type="com.example.jetpack.bean.User" alias="Lenve"/>

        <variable
            name="user"
            type="Lenve" />
    </data>

4. 字符串拼接

如activity_main中如下属性

  android:text="@{`名字`+user.name}"

5. 设置默认值:默认值无需加引号,只在预览视图中显示

由于 TextView在布局文件中并没有明确的值,所以在预览视图中什么都不会显示,不便于观察文本的大小和字体颜色等属性,此时可以为之设定默认值(文本内容或者是字体大小等属性都适用),默认值将只在预览视图中显示,且默认值不能包含引号。

        <TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{`名字`+user.name,default=morenzhi}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

6. 三目运算

       <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{user.phone==null?user.phone:`147522444`}"
            android:textSize="18sp" />

7. 布局中要使用某个类的方法

public class StringUtils {

    public static String getNewStr(String str) {
        return str+"-new";
    }
}

在 data 标签中导入该工具类:

 <import type="com.example.jetpack.StringUtils" />

然后就可以像对待一般的函数一样来调用了:

<TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{StringUtils.getNewStr(userInfo.name)}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

8. 资源引用

dataBinding 支持对尺寸和字符串这类资源的访问。

 <string name="title">标题</string>
    <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{@string/title}"
            android:textColor="#f00"
            android:textSize="20sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    <dimen name="paddingBig">190dp</dimen>
    <dimen name="paddingSmall">150dp</dimen>
<string name="format">%s is %s</string>
    <data>
        <variable
            name="flag"
            type="boolean" />
    </data>       
    <Button
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:paddingLeft="@{flag ? @dimen/paddingBig:@dimen/paddingSmall}"
         android:text='@{@string/format("leavesC", "Ye")}'
         android:textAllCaps="false" />

9. 属性控制

可以通过变量值来控制 View 的属性:

 <TextView
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:text="可见性变化"
     android:visibility="@{user.male  ? View.VISIBLE : View.GONE}" />

10. 避免空指针异常

DataBinding 也会自动帮助我们避免空指针异常:

例如,如果 “@{userInfo.password}” 中 userInfo 为 null 的话,userInfo.password会被赋值为默认值 null,而不会抛出空指针异常。

11. 运算符

DataBinding 支持在布局文件中使用以下运算符、表达式和关键字:

在这里插入图片描述

目前不支持以下操作:

在这里插入图片描述
此外,DataBinding 还支持以下几种形式的调用:

空合并运算符 ?? 会取第一个不为 null 的值作为返回值

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

等价于

android:text="@{user.name != null ? user.name : user.password}"

在 Fragment中使用Databinding

class DemoFrgament : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val binding = DataBindingUtil.inflate<FragmentDemoBinding>(
            inflater,
            R.layout.fragment_demo,
            container,
            false
        )
		
		//或者
		//val binding = FragmentDemoBinding.inflate(inflater, container, false)

        return binding.root
    }
}

DataBinding事件绑定

严格意义上来说,事件绑定也是一种变量绑定,只不过设置的变量是回调接口而已,事件绑定可用于以下多种回调事件:

  • android:onClick
  • android:onLongClick
  • android:afterTextChanged
  • android:onTextChanged

Databinding事件绑定,分两种方式:方法引用和监听绑定,下面分别用案例介绍两种事件绑定的异同。

方式1:直接获取控件设置点击事件

binding.btn1.setOnClickListener {
        Toast.makeText(this,"点击了按钮1",Toast.LENGTH_SHORT).show()
}

方式2:方法引用

传入OnClickListener的变量:

<variable
        name="listener"
        type="android.view.View.OnClickListener" />

在Button中给android:onClick设置listener的变量:

<Button
            android:id="@+id/btn1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{listener}"
            android:text="按钮"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />

        <Button
            android:id="@+id/btn2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{listener}"
            android:text="按钮"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn1" />

通过setListener传入点击监听给listener对象:

    binding.setListener {
            when (it.id) {
                R.id.btn1 -> Toast.makeText(this, "点击了按钮1", Toast.LENGTH_SHORT).show()
                R.id.btn2 -> Toast.makeText(this, "点击了按钮2", Toast.LENGTH_SHORT).show()

            }
        }

方式3:方法引用

   <variable
            name="handlers"
            type="com.example.jetpack.MainActivity" />

调用语法可以是@{handlers::onClickFriend}或者@{handlers.onClickFriend}:

  <Button
            android:id="@+id/btn1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{handlers::onClickFriend}"
            android:text="按钮"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />

        <Button
            android:id="@+id/btn2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{handlers::onClickFriend}"
            android:text="按钮"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn1" />
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        val url =
            "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"
        binding.user = User("赵丽颖", 20, "17817318859", url)

        binding.handlers = this

    }
    
    fun onClickFriend(view: View) {
        when (view.id) {
            R.id.btn1 -> Toast.makeText(this, "点击了按钮1", Toast.LENGTH_SHORT).show()
            R.id.btn2 -> Toast.makeText(this, "点击了按钮2", Toast.LENGTH_SHORT).show()

        }
    }
}

方式4:方法引用

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

        <variable
            name="handlers"
            type="com.example.jetpack.MainActivity.MyClickHandlers" />
    </data>

   <Button
            android:id="@+id/btn1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="改变name属性"
            android:onClick="@{handlers.onClickChangeName}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />

        <Button
            android:id="@+id/btn2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{handlers::onClickChangAage}"
            android:text="改变age属性"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn1" />

在 Activity 内部新建一个类来声明 onClickChangeName() 和 onClickChangAage() 事件相应的回调方法:

class MainActivity : AppCompatActivity() {

    lateinit var user: User

    lateinit var binding: ActivityMainBinding

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

        binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        val url =
            "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"

        user = User("赵丽颖", 20, "17817318859", url)
        binding.user = user

        binding.handlers = MyClickHandlers()

    }

    inner class MyClickHandlers {
        fun onClickChangeName(v: View?) {
            user.name = "赵丽颖2"
            binding.user=user
        }

        fun onClickChangAage(v: View?) {
            user.age = 18
            binding.user=user
        }
    }
}

方式5:方法引用

把回调方法单独写到一个接口。

  <variable
            name="handlers"
            type="com.example.jetpack.UserClickListener" />

 <Button
            android:id="@+id/btn1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="按钮"
            android:onClick="@{handlers.userClicked}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />

        <Button
            android:id="@+id/btn2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{handlers::userClicked}"
            android:text="按钮"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn1" />

interface UserClickListener {

    fun userClicked(view: View?)
}
class MainActivity : AppCompatActivity(), UserClickListener {

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

       val  binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        val url =
            "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"

        val user = User("赵丽颖", 20, "17817318859", url)
        binding.user = user

        binding.handlers = this

    }

    override fun userClicked(view: View?) {
        Toast.makeText(this, "方法引用",Toast.LENGTH_SHORT).show();
    }
}

方式6:监听绑定(重要)

   onclick="@{()->vm.click()}"
   onclick="@{(v)->vm.click(v)}"
   onclick="@{()->vm.click(context)}"
   onclick="@{BindHelp::staticClick}"
   onclick="@{callback}"

将对象直接传回点击方法中。

   <variable
            name="handlers"
            type="com.example.jetpack.MainActivity.MyClickHandlers" />
    <Button
            android:id="@+id/btn1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{()->handlers.showUser(user)}"
            android:text="按钮"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />
class MainActivity : AppCompatActivity() {


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

        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        val url =
            "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"
        var user = User("赵丽颖", 20, "17817318859", url)
        binding.user = user

        binding.handlers = MyClickHandlers()

    }

    inner class MyClickHandlers {
        fun showUser(user: User) {
            Toast.makeText(this@MainActivity, user.name, Toast.LENGTH_SHORT).show()
        }

    }
}

include和viewStub中使用DataBinding

DataBinding不支持merge标签。

include

对于 include 的布局文件,一样是支持通过 dataBinding 来进行数据绑定,此时一样需要在 include 的布局中依然使用 layout 标签,声明需要使用到的变量。

  • view_include.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">

    <data>

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

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

        <TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{`名字`+user.name}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
  • 在主布局文件中将相应的变量传递给 include 布局,从而使两个布局文件之间共享同一个变量:
<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

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

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <include
            layout="@layout/view_include"
            bind:user="@{user}" />


    </LinearLayout>
</layout>

viewStub

dataBinding 一样支持 ViewStub 布局。

在布局文件中引用 viewStub 布局:

如果需要为 ViewStub 绑定变量值,则 ViewStub 文件一样要使用 layout 标签进行布局,主布局文件使用自定义的 bind 命名空间将变量传递给 ViewStub。

<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

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

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <ViewStub
            android:id="@+id/view_stub"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            bind:user="@{user}"
            android:layout="@layout/view_include"/>

    </LinearLayout>
</layout>

获取到 ViewStub 对象,由此就可以来控制 ViewStub 的可见性:

class MainActivity : AppCompatActivity() {

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

        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        val user = User("赵丽颖", 20, "17817318859")
        binding.user = user


        //拿到ViewStub的实例之后,调用inflate()方法将隐藏的布局给加载出来
        val view = binding.viewStub.viewStub?.inflate()
    }

}

如果在 xml 中没有使用 bind:userInfo="@{userInf}"对 ViewStub 进行数据绑定,则可以等到当 ViewStub Inflate 时再绑定变量,此时需要为 ViewStub 设置 setOnInflateListener回调函数,在回调函数中进行数据绑定:

       binding.viewStub.viewStub?.setOnInflateListener { stub, inflated ->

            //如果在 xml 中没有使用 bind:userInfo="@{userInf}" 对 viewStub 进行数据绑定
            //那么可以在此处进行手动绑定
            val viewIncludeBinding = DataBindingUtil.bind<ViewIncludeBinding>(inflated)
            viewIncludeBinding?.user = user
        }

dataBinding 支持在布局文件中使用数组,集合

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

而为了和 variable 标签的尖括号区分开,在声明 Lsit< String > 之类的数据类型时,需要使用尖括号的转义字符。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <import type="java.util.List" />
        <import type="java.util.Map" />
        <import type="java.util.Set" />
        <import type="android.util.SparseArray" />
        <variable
            name="array"
            type="String[]" />
        <variable
            name="list"
            type="List&lt;String&gt;" />
        <variable
            name="map"
            type="Map&lt;String, String&gt;" />
        <variable
            name="set"
            type="Set&lt;String&gt;" />
        <variable
            name="sparse"
            type="SparseArray&lt;String&gt;" />
        <variable
            name="index"
            type="int" />
        <variable
            name="key"
            type="String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".Main7Activity">

        <TextView
            ···
            android:text="@{array[1]}" />
        <TextView
            ···
            android:text="@{sparse[index]}" />
        <TextView
            ···
            android:text="@{list[index]}" />
        <TextView
            ···
            android:text="@{map[key]}" />
        <TextView
            ···
            android:text='@{map["leavesC"]}' />
        <TextView
            ···
            android:text='@{set.contains("xxx")?"xxx":key}' />
    </LinearLayout>
</layout>

BindingAdapter:自定义属性注解

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

  • 作用于方法,这个方法可以写在任何地方。
  • 这个注解面向一个public static方法。
  • {}内部表示使用时这个属性的名字。例如:@BindingAdapter({“imageUrl”}),imageUrl为属性的名称。
  • BindingAdapter也可以设置多个属性。

加载图片

对于一个 ImageView ,我们希望在某个变量值发生变化时,可以动态改变显示的图片,此时就可以通过 BindingAdapter 来实现。

在java中使用

在java中使用dataBinding展示图片很简单,只需要配置一个静态的BindingAdapter就可以了。

需要先定义一个静态方法,为之添加 BindingAdapter 注解,注解值是为 ImageView 控件自定义的属性名,而该静态方法的两个参数可以这样来理解:

当 ImageView 控件的 url 属性值发生变化时,dataBinding 就会将 ImageView 实例以及新的 url 值传递给 loadImage() 方法,从而可以在此动态改变 ImageView 的相关属性。

public class DataBindingUtils {

    @BindingAdapter("imageUrl")  //imageUrl:控件的属性名
    public static void loadImage(ImageView imageView, String url) {
        Glide.with(imageView.getContext())
                .load(url)
                .placeholder(R.mipmap.ic_launcher)
                .error(R.mipmap.ic_launcher)
                .into(imageView);
    }
}
       <ImageView
            android:id="@+id/ivNet"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            app:imageUrl="@{user.url}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn" />

当ImageView中使用imageUrl属性时,会自动调用loadImage方法。

  • BindingAdapter多参数
public class ImageHelper {

    @BindingAdapter({"imageUrl","errorDrawableId","placeDrawableId"})
    public static void loadImage(ImageView imageView,String url,int errorDrawableId,int placeDrawableId ){
        Glide.with(imageView.getContext())
                .load(url)
                .error(errorDrawableId)
                .placeholder(placeDrawableId)
                .into(imageView);

    }
}

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

    <data>

        <variable
            name="url"
            type="String" />
        <variable
            name="url2"
            type="int" />

    </data>
    <LinearLayout
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <ImageView
            android:layout_width="90dp"
            android:layout_height="90dp"
            android:scaleType="centerCrop"
            app:imageUrl="@{url}"
            app:placeDrawableId="@{url2}"
            app:errorDrawableId="@{url2}"/>

    </LinearLayout>
</layout>


  • requireAll
    @android.databinding.BindingAdapter(value = {"app:imgUrl", "app:placeholder"}, requireAll = false)
    public static void loadImg(ImageView imageView, String url, Drawable placeholder) {
        GlideApp.with(imageView)
                .load(url)
                .placeholder(placeholder)
                .into(imageView);
    }

这里 requireAll = false 表示我们可以使用这两个两个属性中的任一个或同时使用,如果requireAll = true 则两个属性必须同时使用,不然报错。默认为 true。

在kotlin中使用

kotlin需要加@JvmStatic注解

首先:kotlin中没有static关键字,但是提供了companion object{}代码块和使用object关键字。

object关键字声明一种特殊的类,这个类只有一个实例,因此看起来整个类就好像是一个对象一样,这里把类声明时的class关键字改成了object,这个类里面的成员默认都是static的。

@JvmStatic注解:与伴生对象搭配使用,将变量和函数声明为真正的JVM静态成员。

要加上kapt插件:

apply plugin: 'kotlin-kapt'
  • 第一种方式:使用 companion object
class DataBindingUtils {

    companion object {
        @BindingAdapter("imageUrl")
        @JvmStatic
        fun loadImage(view: ImageView, url: String) {
            Glide.with(view.context).load(url).into(view)
        }
    }
}

  • 第二种方式:使用object
object DataBindingUtils {
	
	//imageUrl:就是要在布局文件中使用的属性
    @BindingAdapter("imageUrl")
    @JvmStatic
    fun loadImage(view: ImageView, url: String) {
        Glide.with(view.context).load(url).into(view)
    }
}

val url="https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"
binding.user= User("赵丽颖",20,"17817318859",url)

       <ImageView
            android:id="@+id/ivNet"
            android:layout_width="match_parent"
            android:layout_height="300dp"
            app:imageUrl="@{user.url}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn" />

可以覆盖 Android 原先的控件属性

BindingAdapter 更为强大的一点是可以覆盖 Android 原先的控件属性。

例如,可以设定每一个 TextView 的文本都要加上后缀:“-赵丽颖”

    @BindingAdapter("android:text")
    public static void setText(TextView view, String text) {
        view.setText(text + "赵丽颖");
    }

     <TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{`名字`+user.name,default=morenzhi}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

这样,整个工程中使用到了 “android:text” 这个属性的控件,其显示的文本就会多出一个后缀-赵丽颖。

可以用这个属性来加载本地图片:

public class ImageViewAdapter {

    @BindingAdapter("android:src")
    public static void setSrc(ImageView view, Bitmap bitmap) {
        view.setImageBitmap(bitmap);
    }
 
    @BindingAdapter("android:src")
    public static void setSrc(ImageView view, int resId) {
        view.setImageResource(resId);
    }
 
 
    @BindingAdapter("imageUrl")
    public static void setSrc(ImageView imageView, String url) {
        Glide.with(imageView.getContext()).load(url)
                .placeholder(R.mipmap.ic_launcher)
                .into(imageView);
    }
 
 
    @BindingAdapter({"app:imageUrl", "app:placeHolder", "app:error"})
    public static void loadImage(ImageView imageView, String url, Drawable holderDrawable, Drawable errorDrawable) {
        Glide.with(imageView.getContext())
                .load(url)
                .placeholder(holderDrawable)
                .error(errorDrawable)
                .into(imageView);
    }
}

自定义属性

object DataBindingUtils {

    /**
     * View的显示和隐藏
     */
    @BindingAdapter("isGone")
    @JvmStatic
    fun bindISGone(view: View, isGone: Boolean) {
        view.visibility = if (isGone) View.GONE else View.VISIBLE
    }
}
  <data>

        <variable
            name="hasPlantings"
            type="boolean" />

    </data>

  <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/garden_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:paddingLeft="@dimen/margin_normal"
            android:paddingRight="@dimen/margin_normal"
            app:isGone="@{!hasPlantings}"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            tools:listitem="@layout/list_item_garden_planting" />

    
        <TextView
            android:id="@+id/empty_garden"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="@string/garden_empty"
            android:textSize="24sp"
            app:isGone="@{hasPlantings}" />

BindingConversion

dataBinding 还支持对数据进行转换,或者进行类型转换。

数据转换

与 BindingAdapter 类似,以下方法会将布局文件中所有以@{String}方式引用到的String类型变量加上后缀-conversionString。

    @BindingConversion
    public static String conversionString(String text) {
        return text + "-conversionString";
    }
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text='@{"xxx"}'
            android:textAllCaps="false"/>
      <TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{userInfo.name}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

如果同时设置了BindingAdapter 和 BindingConversion ,都会同时生效了,而 BindingConversion 的优先级要高些

   @BindingConversion
    public static String conversionString(String text) {
        return text + "-conversionString";
    }

    @BindingAdapter("android:text")
    public static void setText(TextView view, String text) {
        view.setText(text + "-TextView");
    }

在这里插入图片描述

转换属性值的类型

此外,BindingConversion 也可以用于转换属性值的类型

看以下布局,此处在向 background 和 textColor 两个属性赋值时,直接就使用了字符串,按正常情况来说这自然是会报错的,但有了 BindingConversion 后就可以自动将字符串类型的值转为需要的 Drawable 和 Color 了。

   <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:background="红色"
        android:textColor="蓝色"
        android:layout_height="wrap_content"/>
    @BindingConversion
    public static Drawable convertStringToDrawable(String str) {
        if (str.equals("红色")) {
            return new ColorDrawable(Color.parseColor("#FF4081"));
        }
        if (str.equals("蓝色")) {
            return new ColorDrawable(Color.parseColor("#3F51B5"));
        }
        return new ColorDrawable(Color.parseColor("#344567"));
    }

    @BindingConversion
    public static int convertStringToColor(String str) {
        if (str.equals("红色")) {
            return Color.parseColor("#FF4081");
        }
        if (str.equals("蓝色")) {
            return Color.parseColor("#3F51B5");
        }
        return Color.parseColor("#344567");
    }
  • @BindingConversion比@BindingAdapter先执行。
  • @BindingConversion使用companion object方式定义静态方法,会报异常@BindingConversion is only allowed on public static methods conversionString(String),采用Object方式定义静态方法即可。

BindingMethods

适配扩展支持binding的函数。

BindingMethods包含若干BindingMethod,BindingMethod是BindingMethods的子集。

indingMethods与BindingMethod用于类的注解,简单的可以理解为,定义xml中定义的属性与某个medthod(方法)绑定。

//这里BindMethods可以放在任何的类上面,重点在于内部属性声明
@BindingMethods(
    BindingMethod(
        type = AppCompatImageView::class,
        attribute = "image",
        method = "setImageDrawable"
    )
)
object BdTool {
    @JvmStatic
    fun getTitle(type: String): String {
        return "type:$type"
    }
}

<?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="imgRes"
            type="android.graphics.drawable.Drawable" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.AppCompatImageView
            image="@{imgRes}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            tools:ignore="MissingConstraints" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class MainActivity : AppCompatActivity() {

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

        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        binding.apply {
            imgRes=getDrawable(R.mipmap.ic_launcher)
        }
    }
}

单向绑定

通过DataBinding进行绑定控件以及进行相关操作,但是,这遇到了一个瓶颈,就比如绑定的变量发生变化的时候,每次都要重新向 ViewDataBinding 传值进行更新操作之后才能刷新UI。那么怎么就能自动刷新UI了呢?那就得用 单向绑定 了!

实现数据变化自动驱动 UI 刷新的方式有三种:

  • BaseObservable
  • ObservableField
  • ObservableCollection

使用BaseObservable动态更新UI

一个纯净的 ViewModel 类被更新后,并不会让 UI 自动更新。而数据绑定后,我们自然会希望数据变更后 UI 会即时刷新,Observable 就是为此而生的概念。

BaseObservable 提供了 notifyChange()notifyPropertyChanged() 两个方法,前者会刷新所有的值域,后者则只更新对应 BR 的 flag,该 BR 的生成通过注释 @Bindable 生成,可以通过 BR notify 特定属性关联的视图。

package com.example.jetpack.bean;

import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;

import com.example.jetpack.BR;

/**
 * Cerated by xiaoyehai
 * Create date : 2020/11/14 13:02
 * description :
 * <p>
 * 1.Student继承BaseObservable
 * 2.每个getter()加上注解@Bindable
 * 3.每个setter()加上notifyPropertyChanged(BR.xxx); - BR在rebuild后自动生成
 */
public class UserInfo extends BaseObservable {

    // 如果是 public 修饰符,则可以直接在成员变量上方加上 @Bindable 注解
    private String name;

    //如果是 private 修饰符,则在成员变量的 get 方法上添加 @Bindable 注解
    private String password;

    private String desc;

    public UserInfo(String name, String password, String desc) {
        this.name = name;
        this.password = password;
        this.desc = desc;
    }

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        //只更新本字段:name变化时只会更新新本字段
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;

        //更新所有字段:password会更新所有字段
        notifyChange();
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
}

BR是编译阶段生成的一个类,功能与 R.java 类似,用 @Bindable标记过 getter方法会在BR中生成一个静态常量。

UserInfo用kotlin来实现:

class UserInfo : BaseObservable() {

    //由于kotlin的属性默认是public修饰,所以可以直接在属性上@Bindable, 如何设置了修饰符且不为public的话,
    // 则可使用@get BIndable(表示在get()方法上标记@Bindable)

    // 对name进行@Bindable标志,然后会生成BR.name
    @Bindable
    var name: String = ""
        set(value) {
            field = value
            // 当name,发生改变时只会刷新与name相关控件的值,不会刷新其他的值
            notifyPropertyChanged(BR.name)
        }

    @get: Bindable
    var password: String = ""
        set(value) {
            field = value
            // 当password 发生改变时,也会刷新其他属性相关的控件的值
            notifyChange()
        }


    var desc: String = ""
}
<?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.example.jetpack.bean.UserInfo" />

        <variable
            name="handlers"
            type="com.example.jetpack.MainActivity.MyClickHandlers" />
    </data>

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

        <TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{userInfo.name}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tvPassword"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{userInfo.password}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvName" />

        <TextView
            android:id="@+id/tvDesc"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{userInfo.desc}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvPassword" />


        <Button
            android:id="@+id/btn1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{handlers.onClickChangeName}"
            android:text="改变name和desc"
            android:textAllCaps="false"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvDesc" />

        <Button
            android:id="@+id/btn2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{handlers.onClickChangePassword}"
            android:text="改变password和desc"
            android:textAllCaps="false"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn1" />


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class MainActivity : AppCompatActivity() {

    lateinit var userInfo: UserInfo

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

        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        userInfo = UserInfo("赵丽颖1", "1234561","描述1")
        binding.userInfo = userInfo

        binding.handlers=MyClickHandlers()
    }


    inner class MyClickHandlers {
        fun onClickChangeName(v: View?) {
            userInfo.name = "赵丽颖2"
            userInfo.desc = "描述2"
        }

        fun onClickChangePassword(v: View?) {
            userInfo.name = "赵丽颖3"
            userInfo.password = "123456123"
            userInfo.desc = "描述3"
        }
    }
}

可以看到,name 值的改变没有同时刷新 desc ,而 password 刷新的同时也刷新了name 和 price 。

OnPropertyChangedCallback:属性变更监听

实现了 Observable 接口的类允许注册一个监听器,当可观察对象的属性更改时就会通知这个监听器,此时就需要用到 OnPropertyChangedCallback。

当中 propertyId 就用于标识特定的字段

  userInfo.addOnPropertyChangedCallback(object :Observable.OnPropertyChangedCallback() {
            override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
                when(propertyId) {
                    BR._all-> Log.e("xyh", "BR._all")
                    BR.name-> Log.e("xyh", "BR.name")
                    BR.password-> Log.e("xyh", "BR.password")
                }
            }

        })

使用ObervablueFields动态更新

ObervablueFields,它其实也是为了能够减少代码量,当一个Bean没有那么多属性的时候,我们不需要写这么多的get和set方法,使用ObervablueFields只需要通过两行代码即可实现相同效果。

继承于 Observable 类相对来说限制有点高,且也需要进行 notify 操作,因此为了简单起见可以选择使用 ObservableField。ObservableField 可以理解为官方对 BaseObservable 中字段的注解和刷新等操作的封装,官方原生提供了对基本数据类型的封装,例如 ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble 以及 ObservableParcelable ,也可通过 ObservableField泛型来申明其他类型。

在这里插入图片描述

public class UserInfo  {

    private ObservableField<String> name;

    private ObservableInt age;

    private ObservableField<String> desc;

    public UserInfo(ObservableField<String> name, ObservableInt age, ObservableField<String> desc) {
        this.name = name;
        this.age = age;
        this.desc = desc;
    }

    public ObservableField<String> getName() {
        return name;
    }

    public void setName(ObservableField<String> name) {
        this.name = name;
    }

    public ObservableInt getAge() {
        return age;
    }

    public void setAge(ObservableInt age) {
        this.age = age;
    }

    public ObservableField<String> getDesc() {
        return desc;
    }

    public void setDesc(ObservableField<String> desc) {
        this.desc = desc;
    }
}

对 ObservableField属性值的改变都会立即触发 UI 刷新,概念上与 Observable 区别不大。

class MainActivity : AppCompatActivity() {

    lateinit var userInfo: UserInfo

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

        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        val name = ObservableField<String>("赵丽颖1")
        val age = ObservableInt(20)
        val desc = ObservableField("描述")
        userInfo = UserInfo(name, age, desc)
        binding.userInfo = userInfo

        binding.handlers = MyClickHandlers()

    }


    inner class MyClickHandlers {
        fun onClickChangeName(v: View?) {
            userInfo.name.set("赵丽颖2")
            userInfo.age.set(21)
            userInfo.desc.set("描述2")
        }

        fun onClickChangePassword(v: View?) {
            userInfo.name.set("赵丽颖3")
        }
    }
}

使用ObservableCollection动态更新UI

上面讲到 ObservableField 单向绑定,和 BaseObservable 相比之下,ObservableField 简单了许多,只需要用它的方法即可,提供了自动刷新UI的方法。

如果用到 Map 或者是 List,同样 DataBinding 还提供了 ObservableMap 和 ObservableList,dataBinding 也提供了包装类用于替代原生的 List 和 Map,分别是 ObservableList 和 ObservableMap,当其包含的数据发生变化时,绑定的视图也会随之进行刷新。

下面来基本使用一下吧!

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

        <import type="androidx.databinding.ObservableList" />
        <import type="androidx.databinding.ObservableMap" />

        <!--注意这个地方,一定要用 "&lt;"和 "&gt;",这里不支持尖括号-->
        <variable
            name="list"
            type="ObservableList&lt;String&gt;" />

        <variable
            name="map"
            type="ObservableMap&lt;String,String&gt;" />

        <variable
            name="index"
            type="int"/>
        <variable
            name="key"
            type="String"/>

        <variable
            name="handlers"
            type="com.example.jetpack.MainActivity.MyClickHandlers" />
    </data>

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

        <TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{list.get(index)}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tvPassword"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{list.get(1)}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvName" />

        <TextView
            android:id="@+id/tvDesc"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{map[key]}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvPassword" />


        <Button
            android:id="@+id/btn1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{handlers.onClickChange}"
            android:text="改变数据"
            android:textAllCaps="false"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tvDesc" />


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
class MainActivity : AppCompatActivity() {

    var list: ObservableList<String> = ObservableArrayList()

    var map: ObservableArrayMap<String, String> = ObservableArrayMap()

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

        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        list.add("赵丽颖1")
        list.add("赵丽颖2")
        binding.list = list
        binding.index = 0

        map["name"] = "赵丽颖3"
        map["age"] = "21"
        binding.map = map
        binding.key = "name"

        binding.handlers = MyClickHandlers()

    }


    inner class MyClickHandlers {
        fun onClickChange(v: View?) {
            list[0]="zly1"
            map["name"] = "zly2"
        }
    }
}

双向绑定

Databinding是Google推出的一个支持View与ViewModel绑定的Library,可以说Databinding建立了一个UI与数据模型之间的桥梁,即UI的变化可以通知到ViewModel, ViewModel的变化同样能够通知到UI从而使UI发生改变,大大减少了之前View与Model之间的胶水代码,如findViewById;改变和获取TextView的内容还需要调用setText()、 getText(),获取EditText编辑之后的内容需要调用getText(),而有了Databinding的双向绑定,这些重复的工作都将被省去。下面我们就来看一下如何使用Databinding来双向绑定。

双向绑定的意思即为当数据改变时同时使视图刷新,而视图改变时也可以同时改变数据。

现在要想到,在哪那些方面需要双向绑定,哪那些方面不需要双向绑定,生存还是死亡,这是个问题。

在以上三个单向绑定案例中,貌似双向绑定没多大用处,下面举例一种情况,在输入账号和密码的时候,UI更新同时数据也要更新,这就用到双向绑定,总的来说,双向绑定使用还是不算多的,双向绑定是安卓MVVM架构的基础。

Data Binding本身是不支持双向绑定的,我们想要实现双向绑定首先要改造一下实体类。让实体类继承BaseObservable。

目前双向绑定仅支持如text,checked,year,month,hour,rating,progress等绑定。

假设有一种情况,当我们在EditText里面输入内容的时候,如果此时我们的User已经和EditText关联,那么我们希望当输入框内容改变的时候,User对应的字段也发生变化,反之User发生变化的时候,输入框的内容也会跟着变化。这也是MVVM架构的思想,有了databinding框架,就可以帮我们快速实现一个MVVM架构。

看以下例子,当 EditText 的输入内容改变时,会同时同步到变量 userInfo,绑定变量的方式比单向绑定多了一个等号:android:text=“@={userInfo.name}”

双向绑定语法:

主要就是在布局文件中EditText中加上了一个 = (等于)号。

<?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.example.jetpack.bean.UserInfo" />

    </data>

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

        <EditText
            android:id="@+id/et"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={userInfo.name}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tvName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{userInfo.name}"
            android:textSize="16sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/et" />


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        val user = UserInfo("赵丽颖", 20)
        binding.userInfo = user

数据源可以使用使用 继承BaseObservable 方式:

public class UserInfo extends BaseObservable {

    //如果是 public 修饰符,则可以直接在成员变量上方加上 @Bindable 注解
    //如果是 private 修饰符,则在成员变量的 get 方法上添加 @Bindable 注解
    private String name;

    private int age;

    public UserInfo(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Bindable
    public String getName() {
        return name;
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;

        //只更新本字段
        notifyPropertyChanged(BR.name);

        //更新所有字段
        //notifyChange();
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }
}

数据源也可以使用 ObservableField 方式,来更好的更新UI,使用方式看上面的单向绑定中使用ObservableField 。

InverseBindingAdapter:反向绑定

双向绑定之基于InverseBindingAdapter的反向绑定

RecyclerView中使用DataBinding

activity_main.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>

        <variable
            name="adapter"
            type="com.example.jetpack_demo.RvAdapter" />
    </data>

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

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:itemCount="10"
            tools:listitem="@layout/item_rv" />
        <!--        app:adapter="@{adapter}"-->


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

item.rv.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">

    <data>

        <variable
            name="userBean"
            type="com.example.jetpack_demo.UserInfo" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="10dp">

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

        <TextView
            android:id="@+id/tv_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(userBean.age)}"
            android:textSize="20sp" />

    </LinearLayout>

</layout>


UserInfo

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

MainActivity

class MainActivity : AppCompatActivity() {

    private val binding: ActivityMainBinding by lazy {
        DataBindingUtil.setContentView(this, R.layout.activity_main)
    }

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

        binding.recyclerView.addItemDecoration(
            DividerItemDecoration(
                this,
                DividerItemDecoration.VERTICAL
            )
        )

        var datas = mutableListOf<UserInfo>()
        for (i in 0..29) {
            datas.add(UserInfo("zly$i", 10 + i))
        }
        val rvAdapter = RvAdapter(this, datas)
        binding.recyclerView.adapter = rvAdapter
    }
}

RvAdapter

package com.example.jetpack;
/**
 * Cerated by xiaoyehai
 * Create date : 2020/11/14 15:53
 * description :
 */
public class RvAdapter extends RecyclerView.Adapter<RvAdapter.ViewHolder> {

    private Context mContext;
    private List<UserInfo> mDatas;

    public RvAdapter(Context context, List<UserInfo> datas) {
        mContext = context;
        this.mDatas = datas;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {


        //用父类ViewDataBinding接收也可以,不过使用的时候需要强转
        /*ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext),
                R.layout.item_rv, parent, false);*/

        ItemRvBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext),
                R.layout.item_rv, parent, false);

        //设置点击事件
        binding.getRoot().setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });

        return new ViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {

        /*ViewDataBinding binding = holder.getBinding();
        binding.setVariable(BR.userBean, mDatas.get(position));//绑定数据
        binding.executePendingBindings();//立刻执行绑定,执行刷新*/


        ItemRvBinding binding = holder.getBinding();
        binding.setUserBean(mDatas.get(position));
        binding.tvName.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });

        binding.executePendingBindings();//立刻执行绑定,执行刷新

    }

    @Override
    public int getItemCount() {
        return mDatas.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        ItemRvBinding mDataBinding;

        public ViewHolder(ItemRvBinding binding) {
            super(binding.getRoot());
            mDataBinding = binding;
        }

        public ItemRvBinding getBinding() {
            return mDataBinding;
        }
    }
}

RvAdapter Kotlin写法:

class RvAdapter(private val context: Context, private val data: List<UserInfo>) :
    RecyclerView.Adapter<RvAdapter.ViewHolder>() {


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding: ItemRvBinding =
            DataBindingUtil.inflate(
                LayoutInflater.from(context),
                R.layout.item_rv,
                parent,
                false
            )

        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.binding.userBean = data[position]
        //holder.binding.setVariable(BR.userBean,data[position])
        holder.binding.executePendingBindings() //立刻执行绑定,执行刷新
    }

    override fun getItemCount(): Int = data.size

    class ViewHolder(var binding: ItemRvBinding) : RecyclerView.ViewHolder(binding.root)

}

LisView中使用DataBinding

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

        <import type="com.xiaoyehai.mvvm.adapter.LvAdapter" />

        <variable
            name="adapter"
            type="LvAdapter" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".activity.LvActivity">

        <ListView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:adapter="@{adapter}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

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

条目布局:

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

    <data>

        <variable
            name="user"
            type="com.xiaoyehai.mvvm.entity.UserBean" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <ImageView
            android:layout_width="100dp"
            app:imageUrl="@{user.imgUrl}"
            android:layout_height="100dp" />

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:text="@{user.name}"
            android:textSize="20sp" />

    </LinearLayout>

</layout>

/**
 * Created by : xiaoyehai
 * Create date : 2019/10/14 14:21
 * description :
 */
public class LvAdapter extends BaseAdapter {

    private Context mContext;
    private List<UserBean> mDatas;
    private int mLayoutId;

    private final LayoutInflater mLayoutInflater;

    public LvAdapter(Context context, List<UserBean> datas, int layoutId) {
        mContext = context;
        this.mDatas = datas;
        this.mLayoutId = layoutId;
        mLayoutInflater = LayoutInflater.from(context);
    }

    @Override
    public int getCount() {
        return mDatas.size();
    }

    @Override
    public Object getItem(int position) {
        return mDatas.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ItemLvBinding binding;
        ViewHolder holder;
        if (convertView == null) {
            //获取item布局的binding
            binding = DataBindingUtil.inflate(mLayoutInflater, mLayoutId, parent, false);
            //获取布局
            convertView = binding.getRoot();
            holder = new ViewHolder();

            //缓存binding到holder
            holder.setItemLvBinding(binding);

            //设置Tag
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
            binding = (ItemLvBinding) holder.getItemLvBinding();
        }
        binding.setUser(mDatas.get(position));
        return convertView;
    }

    /**
     * viewholder类里只有一个binding对象和它的get,set方法
     */
    private class ViewHolder {
        private ViewDataBinding binding;

        public void setItemLvBinding(ViewDataBinding binding) {
            this.binding = binding;
        }

        public ViewDataBinding getItemLvBinding() {
            return binding;
        }
    }

}

DataBinding原理和源码解析

原理

DataBinding使用了Gradle插件+APT技术,我们build项目时DataBinding会生成多个文件。

APT生成的目录结构

首先来看看ActivityMainBinding是什么时间生成的?

回顾一下,在activity_main.xml中把根布局改为layout标签之后,在回到对应的MainActivity就可以使用ActivityMainBinding对象了。这一点也可以得到证实:在引入layout之后,build项目会发现在Project目录下生成多个新的文件,目录大致如下:

在这里插入图片描述

此外,DataBinding还将原有的activity_main.xml文件进行了拆分,分别是activity_mian.xml和activity_main-layout.xml。

原有的activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <!--DataBinding的配置文件-->
    <data>
        <!--name属性相当于声明了一个全局属性、type指定name对应的具体的数据类或是MVVM中VM-->
        <variable
            name="user"
            type="com.zly.ktdemo.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="50dp">

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

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(user.age)}" />

        <View
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
</layout>

拆分后的activity_mian.xml(Android OS 渲染的布局文件):

在这里插入图片描述
在这里插入图片描述

通过上面的代码我们发现DataBinding将原有的layout和data标签去除了。并为根布局声明了一个layout/文件名_0的tag,为其他使用到@{}或@={}的控件按顺序添加了一个binding_X的tag。

拆分后的activity_main-layout.xml(DataBinding需要的布局控件信息):

在这里插入图片描述

在这里插入图片描述

  //我们声明的全局变量
  <Variables name="user" declared="true" type="com.zly.ktdemo.User">
      <Target tag="binding_1" view="EditText"> //tag对应的View类型
            <Expressions>
            	//控件绑定具体属性和Model中的具体属性
                <Expression attribute="android:text" text="user.name">
                    <Location endLine="19" endOffset="39" startLine="19" startOffset="12" />
                    <TwoWay>true</TwoWay> //是否是双向的
                    <ValueLocation endLine="19" endOffset="37" startLine="19" startOffset="29" />
                </Expression>
            </Expressions>
            <location endLine="19" endOffset="42" startLine="16" startOffset="8" />
        </Target>

结合生成文件和XML的位置和时间节点,大致可以看出生成的原理是Gradle插件+APT,这个插件是Gradle内置的,目前还没有查找到相关插件的实现在哪里。

数据是如何绑定(更新)到View的?

val binding =
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

通过activity的setContentView加载布局,并通过window找到id为content的ViewGroup,它是一个FrameLayout用于加载我们添加的布局文件。接下来就是bindToAddedViews方法。

    private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
            ViewGroup parent, int startChildren, int layoutId) {
        final int endChildren = parent.getChildCount();
        final int childrenAdded = endChildren - startChildren;
        if (childrenAdded == 1) {
            final View childView = parent.getChildAt(endChildren - 1);
            return bind(component, childView, layoutId);
        } else {
            final View[] children = new View[childrenAdded];
            for (int i = 0; i < childrenAdded; i++) {
                children[i] = parent.getChildAt(i + startChildren);
            }
            return bind(component, children, layoutId);
        }
    }
public class DataBindingUtil {
    private static DataBinderMapper sMapper = new DataBinderMapperImpl();
    private static DataBindingComponent sDefaultComponent = null;
    
   public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
            int layoutId) {
        return setContentView(activity, layoutId, sDefaultComponent);
    }
    
   public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
            int layoutId, @Nullable DataBindingComponent bindingComponent) {
        activity.setContentView(layoutId);
        View decorView = activity.getWindow().getDecorView();
        ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
        return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
    }
}

parent中的子View就是我们布局文件中的根布局LinearLayout,所以走的是if中的代码

    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId);
    }

    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }

先看看大致的调用关系,找到最终绑定View的地方:

setContentView
----->DataBinderMapperImpl#getDataBinder()
----->根据根布局标记的tag ActivityMainBindingImpl
----->内部构造方法调用invalidateAll();
----->mRebindRunnablemRebindRunnable
---->executePendingBindings()
---->executeBindingsInternal()
----> executeBindings()

数据绑定的具体实现是在ActivityMainBindingImpl#executeBindings()方法中:

   //绑定的数据一旦变化,就会调进这个方法
    //我们也可以调用自动生成的XXXBinding.executePendingBindings(),主动调入该函数来刷新UI
    @Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;
        }
        java.lang.String userName = null;
        int userAge = 0;
        com.zly.ktdemo.User user = mUser;
        java.lang.String stringValueOfUserAge = null;

        if ((dirtyFlags & 0x7L) != 0) {



                if (user != null) {
                    // read user.name
                    userName = user.getName();
                }
            if ((dirtyFlags & 0x5L) != 0) {

                    if (user != null) {
                        // read user.age
                        userAge = user.getAge();
                    }


                    // read String.valueOf(user.age)
                    stringValueOfUserAge = java.lang.String.valueOf(userAge);
            }
        }
        // batch finished
        if ((dirtyFlags & 0x7L) != 0) {
            // api target 1

            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, com.zly.ktdemo.DataBindingUtils.conversionString(userName));
        }
        if ((dirtyFlags & 0x4L) != 0) {
            // api target 1

            androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView1, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView1androidTextAttrChanged);
        }
        if ((dirtyFlags & 0x5L) != 0) {
            // api target 1

            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, com.zly.ktdemo.DataBindingUtils.conversionString(stringValueOfUserAge));
        }
    }

这里通过TextViewBindingAdapter.setText()去给UI控件赋值;而且在赋值之前会对model对象进行判空,这样就避免了set XX()方法时出现空指针异常。

到这里还没完,这个地方只能算是完成数据到View的映射,当我们更改Model的ObservableField属性去更新数据的时候,又是如何更新UI的呢?

ObservableField是对常用数据结构对包装类,它最终继承BaseObservable,它内部封装了观察者模式,可以监听数据的变化。

看一下它的set方法:

   /**
     * Set the stored value.
     */
    public void set(T value) {
        if (value != mValue) {
            mValue = value;
            notifyChange();
        }
    }

这里notifyChange()之后会通过观察者模式的OnPropertyChangedCallback回调到ViewDataBinding #WeakListListener#onChanged()。

 private static class WeakListListener extends ObservableList.OnListChangedCallback
            implements ObservableReference<ObservableList> {
        final WeakListener<ObservableList> mListener;

        public WeakListListener(ViewDataBinding binder, int localFieldId) {
            mListener = new WeakListener<ObservableList>(binder, localFieldId, this);
        }

        @Override
        public WeakListener<ObservableList> getListener() {
            return mListener;
        }

        @Override
        public void addListener(ObservableList target) {
            target.addOnListChangedCallback(this);
        }

        @Override
        public void removeListener(ObservableList target) {
            target.removeOnListChangedCallback(this);
        }

        @Override
        public void onChanged(ObservableList sender) {
            ViewDataBinding binder = mListener.getBinder();
            if (binder == null) {
                return;
            }
            ObservableList target = mListener.getTarget();
            if (target != sender) {
                return; // We expect notifications only from sender
            }
            binder.handleFieldChange(mListener.mLocalFieldId, target, 0);
        }

然后

handleFieldChange
—>requestRebind
---->executePendingBindings()
---->executeBindingsInternal()
----> executeBindings()

最终又调用到了最终调用了executeBindings(),这里就是完成UI更新的地方。

观察View变化更新Model数据

再次回到更新UI的地方ActivityMainBindingImpl#executeBindings()。

在这里插入图片描述

 androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView1,
                    (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged) null,
                    (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged) null,
                    (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged) null,
                    mboundView1androidTextAttrChanged);

看一下这个setTextWatcher()方法,当数据发生变化的时候,TextWatcher在回调onTextChanged()的最后,会通过回调传入的ActivityMainBindingImpl # mboundView1androidTextAttrChanged # onChange()

  private androidx.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new androidx.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
            // Inverse of user.name
            //         is user.setName((java.lang.String) callbackArg_0)
            java.lang.String callbackArg_0 = androidx.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
            // localize variables for thread safety
            // user.name
            java.lang.String userName = null;
            // user != null
            boolean userJavaLangObjectNull = false;
            // user
            com.zly.ktdemo.User user = mUser;


            userJavaLangObjectNull = (user) != (null);
            if (userJavaLangObjectNull) {


                user.setName(((java.lang.String) (callbackArg_0)));
            }
        }
    };

这里就比较简单了,获取控件中最新的值,然后给ObservableField属性赋值。

;