[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()`調用,請考慮改用視圖綁定。其模式示意圖:

和視圖綁定類似,對于`Android Studio`的版本也有要求:
> Android Studio 3.4 or greater
# 2. 使用
## 2.1 環境準備
類似的,直接在配置文件中添加:
~~~
android {
...
dataBinding {
enabled = true
}
}
~~~
將之前的`layout`布局文件修改為`DataBinding layout`。直接右擊根布局的標簽元素,然后選擇**Show Context Actions**:

然后就可以看見提供了直接轉換到`data binding`布局的選項:

比如我這里轉換后的`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."
}
}
~~~
結果:

## 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`中顯示的文本并沒有觀測到數據的變化。觀察日志:

其實,這是因為我們設置的數據并不可觀測。我們需要讓數據可以**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>()
~~~
以及基本的:

### 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
}
}
}
~~~
效果:

### 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`中的類似,但是這里比較好奇的是難道這里也會掃描所有包/類中的注解?應該是的,等儲備知識夠了再深入。