ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 1. 说明 > 本篇博客参考[Data Binding in Android (google.cn)](https://developer.android.google.cn/codelabs/android-databinding?hl=en#0) 和 [数据绑定库](https://developer.android.google.cn/topic/libraries/data-binding) 数据绑定可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。其实有点类似于`MVVM`框架,数据和显示的部分动态绑定,**当数据发生改变对应的视图也随之改变**。如果您使用数据绑定的主要目的是取代`findViewById()`调用,请考虑改用视图绑定。其模式示意图: ![](https://img.kancloud.cn/86/d4/86d47eb3ea048e29ca22273bf39c8f41_624x193.png) 和视图绑定类似,对于`Android Studio`的版本也有要求: > Android Studio 3.4 or greater # 2. 使用 ## 2.1 环境准备 类似的,直接在配置文件中添加: ~~~ android { ... dataBinding { enabled = true } } ~~~ 将之前的`layout`布局文件修改为`DataBinding layout`。直接右击根布局的标签元素,然后选择**Show Context Actions**: ![](https://img.kancloud.cn/ce/09/ce09916a8288d3c055aeb7b6203ca711_615x86.png) 然后就可以看见提供了直接转换到`data binding`布局的选项: ![](https://img.kancloud.cn/9e/a3/9ea3e4b166cf90a478c55a18f5769ba1_712x184.png) 比如我这里转换后的`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"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="内容" android:textSize="24sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.194" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> ~~~ 在`data`标签中的内容,也就是定义的变量。比如可以定义如下的两个变量: ~~~ <data> <import type="android.view.View"/> <variable name="name" type="String"/> <variable name="message" type="String"/> </data> ~~~ 因为后续需要使用`View`,所以这里需要导入包。对应的,可以使用自定义的类,然后导入对应的包即可。 ## 2.2 根据name长度显示Message案例 将定义的变量和布局文件中的控件关联,也就是使用变量。在`Android Jetpack`中定义的使用方式为`@{ expression }`的格式,也就是可以如下使用: ~~~ <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{ message }" android:visibility="@{ (name.length > 3) ? View.VISIBLE : View.GONE }" ... /> ~~~ 然后就是在代码中设置在`xml`中申明的两个变量的值。和`viewbinding`类似在`databinding`中也需要在`onCreate`方法替换: ~~~ setContentView(R.layout.plain_activity) ~~~ 这里替换为: ~~~ binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main ) ~~~ 所获得的`binding`对象也就是和布局文件相关联的类,即:`ActivityMainBinding`。通过`binding`这个实例,就能够直接操作在`xml`中声明的变量: ~~~ binding.name = "testDemo" binding.message = "Hello data binding." ~~~ 完整代码: ~~~ class MainActivity2 : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 1. setContentView(R.layout.plain_activity) replace with below: // data binding binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main ) // 2. set the variable values binding.name = "testDemo" binding.message = "Hello data binding." } } ~~~ 结果: ![](https://img.kancloud.cn/68/1b/681b41970ea27899281cd685e404219e_185x61.png) ## 2.3 响应点击事件 通常我们可以直接在`xml`中直接设置点击函数,比如: ~~~ android:onClick="onButtonClick" ~~~ 然后在`Activity`中定义方法`onButtonClick`。或者直接通过这个按钮的实例对象来注册监听,进行事件处理。这里也是类似,可以在`xml`中采用`Lambda`表达式的方式来注册函数。首先定义一个`SimpleViewModel`类,继承自`ViewModel`类,如下: ~~~kotlin /** * @author 梦否 on 2022/3/29 * @blog https://mengfou.blog.csdn.net/ */ class SimpleViewModel: ViewModel() { // 定义消息 var message = "Hello data binding." get() { return if(clickNumber % 2 == 0) "偶数" else "奇数" } private set // 阻止外部修改,只支持内部修改 val name = "testDemo" var clickNumber = 0 private set // 定义点击函数 fun onTextViewClick(){ clickNumber++ Log.e("TAG", "onTextViewClick: ${clickNumber}" ) } } ~~~ 但是,很不幸,点击`TextView`之后,在`TextView`中显示的文本并没有观测到数据的变化。观察日志: ![](https://img.kancloud.cn/49/1f/491fbabca94357f6588cffc8b645416d_1010x96.png) 其实,这是因为我们设置的数据并不可观测。我们需要让数据可以**observable**才行。为了让字段可观测,可以使用`observable`类或者`LiveData`。关于可观察的数据对象在`Google`中有详细说明:[使用可观察的数据对象](https://developer.android.google.cn/topic/libraries/data-binding/observability)。 ## 2.4 可观察数据类型 可观察类有三种不同类型:对象、字段和集合。 ### 2.4.1 可观测对象 实现`Observable`接口的类允许注册监听器,以便它们接收有关可观察对象的属性更改的通知。为便于开发,数据绑定库提供了用于实现监听器注册机制的`BaseObservable`类。实现`BaseObservable`的数据类负责在属性更改时发出通知。具体操作过程是向 getter 分配`Bindable`注释,然后在 setter 中调用`notifyPropertyChanged()`方法。 ### 2.4.2 可观测字段 ~~~ ObservableField<String>() ~~~ 以及基本的: ![](https://img.kancloud.cn/75/cc/75cc705955da208b287836fb9af730b0_275x408.png) ### 2.4.3 可观察集合 `ObservableArrayMap`、`ObservableArrayList`等。 ## 2.5 设置数据可观察 因为这里所使用的为基本类型,比如`message`和`name`。由于这里我只需要`message`可观测,所以这里对其应用可观测字段即可。如下: ~~~kotlin /** * @author 梦否 on 2022/3/29 * @blog https://mengfou.blog.csdn.net/ */ class SimpleViewModel : ViewModel() { // 定义可观测的字段,使用ObservableField var message = ObservableField<String>("Hello data binding.") private set // 阻止外部修改,只支持内部修改 val name = "testDemo" var clickNumber = 0 private set // 定义点击函数 fun onTextViewClick() { clickNumber++ if (clickNumber.rem(2) == 0) message.set("is Even") else message.set("is odd") Log.e("TAG", "onTextViewClick: ${clickNumber}") } } ~~~ 对应的修改在`xml`中的`<data>`标签中的变量声明: ~~~xml <data> <import type="android.view.View" /> <variable name="viewModel" type="com.weizu.jetpackdemo.SimpleViewModel" /> </data> ~~~ 对应的`MainActivity`文件: ~~~kotlin class MainActivity2 : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 1. setContentView(R.layout.plain_activity) replace with below: // data binding binding = DataBindingUtil.setContentView<ActivityMainBinding>( this, R.layout.activity_main ) // set data binding.viewModel = SimpleViewModel() } } ~~~ 然后就可以看见点击后奇数点击和偶数点击的切换显示文本效果。 ## 2.6 数据双向绑定 在这个案例中需要达到的效果为:对应定义的可观测字段`Field`内容的改变可以通知到对应的控件,而控件的内容变化也可以通知到`Field`。所以这里可以使用控件`EditText`。 ### 2.6.1 方式一:继承自BaseObservable 在布局文件中定义一个`EditText`和`TextView`,如下: ~~~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="myViewModel" type="com.weizu.jetpackdemo.databinding.MyViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".databinding.MainActivity"> <EditText android:id="@+id/editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" android:text="@={ myViewModel.userInput }" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{ myViewModel.userInput }" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.675" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> ~~~ 值得注意的是,在`EditText`中设置为: ```xml android:text="@={ myViewModel.userInput }" ``` 而在`TextView`中为: ```xml android:text="@{ myViewModel.userInput }" ``` 因为在`EditText`中我们需要完成双向绑定,即用户输入可以通知到`LiveData`,而在`TextView`中只要加载变化后的数据即可。 那么,在自定义`ViewModel`中为: ~~~kotlin /** * @author 梦否 on 2022/4/20 * @blog https://mengfou.blog.csdn.net/ */ class MyViewModel :BaseObservable(){ // 设置为LiveData,便于布局文件中TextView内容的自动更新 private val userInput = MutableLiveData<String>("Tom") // 这里一定不要忘记添加注解@Bindable,否则双向绑定不会生效 @Bindable @JvmName("getUserInput") fun getUserInput(): String{ return userInput.value.toString() } // 用于更新TextView fun get(): LiveData<String> { return this.userInput } @JvmName("setUserInput") fun setUserInput(str: String){ if(!str.equals(userInput)) { this.userInput.value = str } Log.e("TAG", "setValue: ${str}" ) // 通知数据发生了改变 notifyPropertyChanged(BR.myViewModel) // build后会自动生成一个BR类,对应在xml中声明的变量 } } ~~~ 这里为了完成双向绑定,继承自`BaseObservable`,且在`get`方法上使用了`@Bindable`注解来表示绑定。至于`get()`方法仅是为了返回`LiveData`对象,然后在`Activity`中设置观察,更新`TextView`控件内容: ~~~kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityMain2Binding>( this, R.layout.activity_main2 ) // 这里直接使用new一个对象 // 因为这里的自定义ViewModel继承的是BaseObservable类,不是ViewModel类 val myViewModel = MyViewModel() binding.myViewModel = myViewModel // 设置观察,以更新TextView文本 myViewModel.get().observe(this) { binding.textView2.text = myViewModel.get().value } } } ~~~ 效果: ![](https://img.kancloud.cn/a8/ab/a8ab56259e30c35cfedbf17c4b0b69d4_274x158.png) ### 2.6.2 方式二:继承自ObservableField 布局文件还是保持不变: ~~~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="myViewModel" type="com.weizu.jetpackdemo.databinding.MyViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".databinding.MainActivity"> <EditText android:id="@+id/editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" android:text="@={ myViewModel.userInput }" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{ myViewModel.userInput }" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.675" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> ~~~ 对于`ViewModel`进行删减: ~~~kotlin class MyViewModel { // 设置为可观察类型 val userInput = ObservableField<String>("Tom") } ~~~ 最后在`Activity`中进行设置数据: ~~~kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityMain2Binding>( this, R.layout.activity_main2 ) // 这里直接使用new一个对象 // 因为这里的自定义ViewModel继承的是BaseObservable类,不是ViewModel类 val myViewModel = MyViewModel() binding.myViewModel = myViewModel // 在xml文件:@=操作符进行双向绑定 } } ~~~ 达到的效果和上小节一样。 ## 2.7 RecyclerView+dataBinding 可以使用`databinding`来设置每个`item`的内容。比如在主布局文件: ~~~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"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".recycleview.MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> ~~~ 因为使用了`RecyclerView`,所以这里还是定义对应的适配器: ~~~kotlin /** * @author 梦否 on 2022/4/20 * @blog https://mengfou.blog.csdn.net/ */ class MyRecycleViewAdapter(var context: Context, var datas: List<User>) : RecyclerView.Adapter<MyRecycleViewAdapter.MyViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { val inflater = LayoutInflater.from(context) val binding = DataBindingUtil.inflate<RecyclerviewItemBinding>( inflater, R.layout.recyclerview_item, parent, false ) val myViewHolder = MyViewHolder(binding.root) myViewHolder.binding = binding return myViewHolder } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { holder.binding?.user = datas[position] Log.e("TAG", "onBindViewHolder: ${position} + ${ datas[position].name }") } override fun getItemCount(): Int { return datas.size } inner class MyViewHolder(var root: View) : RecyclerView.ViewHolder(root) { var binding: RecyclerviewItemBinding? = null } } ~~~ 同样的在`R.layout.recyclerview_item`布局文件中设置`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> <variable name="user" type="com.weizu.jetpackdemo.recycleview.User" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textView3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{ user.name }" app:layout_constraintBottom_toTopOf="@+id/textView4" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_begin="93dp" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_begin="20dp" /> <TextView android:id="@+id/textView4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{ String.valueOf(user.age) }" app:layout_constraintBottom_toTopOf="@+id/guideline3" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ImageView android:id="@+id/imageView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="1dp" android:imageSrc="@{ user.image }" app:layout_constraintEnd_toStartOf="@+id/textView4" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> ~~~ 对于`User`类比较简单: ~~~kotlin class User(var age: Int, var name: String) { var image = "https://i1.hdslb.com/bfs/face/7e72c58637ff26df68fb30939de078d2bbbfcdbe.jpg" } ~~~ 在主`Activity`中配置: ~~~kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityMain3Binding>( this, R.layout.activity_main3 ) val datas = listOf<User>( User(12, "Jack"), User(10, "Tom"), User(23, "Joe") ) // 必须设置布局管理器,否则不会显示RecyclerView binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.adapter = MyRecycleViewAdapter(this, datas) } } ~~~ 运行即可看见效果。 # 3. 自定义BindingAdapter 参考视频地址:[https://www.bilibili.com/video/BV1Ry4y1t7Tj?p=12](https://www.bilibili.com/video/BV1Ry4y1t7Tj?p=12) 这个案例感觉比较典型,达到的效果为可以使用`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> <variable name="src" type="String" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".BindingAdapterActivity"> <ImageView android:id="@+id/imageView" android:layout_width="300dp" android:layout_height="300dp" app:imageSrc="@{ src }" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> ~~~ 注意到,在`ImageView`标签中直接设置了自定义的字段: ```xml app:imageSrc="@{ src }" ``` 而这个字段以前我们是需要使用`tool:`并定义对应的`styleable`样式。这里并不需要,仅需要使用注解来申明: ~~~kotlin class ImageViewCus { // 需要注意的是,这里需要使用静态方法 companion object{ @JvmStatic @BindingAdapter("app:imageSrc") fun loadImage(imageView: ImageView, str: String){ Glide.with(imageView.context) .load(str) .placeholder(R.drawable.ic_launcher_background) .into(imageView) } } } ~~~ 最后是在`Activity`中使用: ~~~kotlin class MyBindingAdapterActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = DataBindingUtil.setContentView<ActivityBindingAdapterBinding>( this, R.layout.activity_binding_adapter ) binding.src = "https://img-blog.csdnimg.cn/5690c131d90e460fa4c96bf86b1ae634.png" } } ~~~ 传入`databinding`中声明的字符串即可,就可以达到预期的效果。整体的使用流程感觉和`SpringBoot`中的类似,但是这里比较好奇的是难道这里也会扫描所有包/类中的注解?应该是的,等储备知识够了再深入。