---
# System prepended metadata

title: Data Binding
tags: [Android, Jetpack]

---

###### tags: `Android` `Jetpack`
# Data Binding
Jetpack 架構系列之一，Data Binding 是實現資料與 View 綁定的框架
當資料更新時，它能直接自動刷新被綁定的 View，降低了佈局和邏輯的耦合性，使邏輯更清晰
同時能省去 findViewById()、setText() 等步驟，大量減少 Activity 内的程式碼
資料能單向、雙向綁定到 layout 中，能防止内存洩漏

有了 Data Binding，在 Android 中可以更方便的實現 MVVM 架構
因為它讓 ViewModel 與 View 之間有了協作的方式

## 基本使用
<font color=Tomato>以下使用 Kotlin、MVVM 架構實作獲取資料的功能</font>

### 匯入 DataBinding
在 ==build.gradle(Module:app)== 加入以下程式碼，加入後 Sync 並且 Rebuild 專案
```
plugins {
    //...
    
    id 'kotlin-kapt'
}

android {
    //...

    dataBinding {
        enabled = true
    }
}
```

### 應用於 MVVM 架構
先參考此篇文章的 [建立基本的 MVVM](https://hackmd.io/eVxZl0yJQ6Cn5DWPyGEM4A?view#%E5%BB%BA%E7%AB%8B%E5%9F%BA%E6%9C%AC%E7%9A%84-MVVM)，完成後再來加入 Data Binding

==MainViewModel==

加入 [ObservableField](https://www.jianshu.com/p/b9af64c9fa04) 存放資料，isLoading 用來控制 progressbar 是否顯示
mData 用來給予 textView 文字，在 refresh() 中用 set() 更新它們的數值
isLoading 和 mData 要開 public 才可以被訂閱(觀察)

```kotlin=
class MainViewModel {
    private val dataModel = DataModel()

    val mData = ObservableField<String>()
    val isLoading = ObservableField(false)

    fun refresh() {
        isLoading.set(true)

        dataModel.retrieveData(object : DataModel.onDataReadyCallback {
            override fun onDataReady(data: String) {
                mData.set(data)
                isLoading.set(false)
            }
        })
    }
}
```

==activity_main==

點選外層後使用快捷鍵 option + enter 加入 ==layout== 標籤，同時系統會自動生成 ==data== 標籤


![](https://hackmd.io/_uploads/ByHw-YRVh.png)

layout 標籤：用於轉換成 data binding layout
data 標籤：用來綁定 view model 的資料
import 標籤：有用到其他的類別都要在此宣告
variable 標籤：宣告 binding 中的變數及型態

因為用到 View.VISIBLE，須在 data 中 import 否則會報錯
然後在 variable 宣告一個 viewModel 的變數，型態是 MainViewModel 
最後，透過 ==@{}== 的語法，讓元件指定到資料來源

```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>
        <import type="android.view.View"/>

        <variable
            name="viewModel"
            type="com.example.mvvm.MainViewModel" />
    </data>

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

        <Button
            android:id="@+id/btn_refresh"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Refresh"
            android:enabled="@{viewModel.isLoading() ? false : true}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="@{viewModel.isLoading() ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.mData}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

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

==MainActivity==

當 layout 改成 data binding layout 後，會自動生成 Binding 類別
例如：activty_main.xml 就會生成 ActivityMainBinding

將原本的 setContentView 改成 DataBindingUtil.setContentView 並初始化 binding
然後指定 viewModel，接著就可以用 binding 去獲取元件
```kotlin=
class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MainViewModel
    private lateinit var binding: ActivityMainBinding

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

        viewModel = MainViewModel()
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = viewModel

        binding.btnRefresh.setOnClickListener {
            viewModel.refresh()
        }
    }
}
```

:::danger
這樣就完成在 MVVM 中使用 Data Binding 了，但會遇到生命週期的問題，例如螢幕旋轉時，會再進入 onCreate，然後產生新的 MainViewModel，此時新的 MainViewModel 並無資料，因此畫面會呈現空白，而要解決這個問題，可以用 [Jetpack 的 ViewModel](https://hackmd.io/eVxZl0yJQ6Cn5DWPyGEM4A?view#Jetpack-ViewModel) 來處理
:::

## 資料綁定
上述範例其實已經做到資料綁定了，在這節中會分享其他類型的資料綁定

### ObservableCollection
原先我們使用 String、Boolean 類型的可觀察欄位，而集合也可以被觀察，例如：List、Map
當集合的元素有更動時，就會觸發觀察，通知 View 去刷新 UI

修改 ==MainViewModel==
把 mData 改成 ObservableArrayList 類型，並亂數產生數字，加入到 mData
```kotlin=
class MainViewModel {
    private val dataModel = DataModel()

    val mData = ObservableArrayList<String>()
    val isLoading = ObservableField(false)

    fun refresh() {
        isLoading.set(true)

        dataModel.retrieveData(object : DataModel.onDataReadyCallback {
            override fun onDataReady(data: String) {
                val random = (Math.random() * 10).toInt() + 1
                mData.add("$data: $random")
                isLoading.set(false)
            }
        })
    }
}
```

修改 ==activity_main== 的 ==textView==
當集合內有元素時，才去獲取最後一個元素並轉成文字，否則預設為空字串
```xml=
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{viewModel.mData.size() > 0 ? String.valueOf(viewModel.mData.get(viewModel.mData.size() - 1)) : ""}'
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
```

:::danger
注意，因為是在 android:text 中用 @{}，所以資料必須是字串，如果資料不是字串
要記得用 String.valueOf 轉換，因為我們是使用 string 所以其實不需要轉換
如果要預設為空字串，必須要用保留字的特性，外層的 @{} 用單引號，空字串用雙引號表示
另外，如果要顯示中文，必須將中文放在 string 檔案中再引用到 @{} 中
:::

### 雙向綁定
以上我們所使用的其實都只是單向的綁定，意思是我們只有做資料更新時通知 View 刷新
但實際上可以做到雙向綁定，即當 View 的屬性更動時，通知被綁定的資料做更新

https://medium.com/jastzeonic/android-data-binding-%E7%9A%84%E9%9B%99%E5%90%91%E7%B6%81%E5%AE%9A-1516383c4315

### BindingAdapter
https://ithelp.ithome.com.tw/articles/10220908
https://windsuzu.github.io/learn-android-databinding/

## 事件處理
Data Binding 也可以讓 View 對應到 View Model 中的方法，所以能省略 Activity 的點擊事件
接下來，我們把點擊事件改成用 layout 的 onClick 執行

修改 ==activity_main== 的 ==btn_refresh==
```xml=
<Button
    android:id="@+id/btn_refresh"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Refresh"
    android:enabled="@{viewModel.isLoading() ? false : true}"
    android:onClick="@{() -> viewModel.refresh()}"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
```

移除原本在 ==MainActivity== 的點擊事件，程式碼更簡潔了！
```kotlin=
class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MainViewModel
    private lateinit var binding: ActivityMainBinding

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

        viewModel = MainViewModel()
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = viewModel
    }
}
```

:::success
官方提供了兩種事件處理的方式，分別為 Method references、Listener bindings
雖然兩者很相似，但 Listener bindings 可更自由的撰寫表達式
以上範例也是用 Listener bindings 的方式完成
:::

### 傳遞參數
先分析一下上面處理事件的表達式
```xml=
android:onClick="@{() -> viewModel.refresh()}"
```

仔細一看，會發現它其實是 Lambda，接著我們把它修改成傳遞點擊的 View 本身
```xml=
android:onClick="@{(v) -> viewModel.refresh(v)}"
```

最後修改 ==MainViewModel== 中的 ==refresh== 方法
```kotlin=
fun refresh(view: View) {
    isLoading.set(true)

    dataModel.retrieveData(object : DataModel.onDataReadyCallback {
        override fun onDataReady(data: String) {
            mData.set(data)
            isLoading.set(false)
        }
    })
}
```

:::success
如果想傳遞其他參數，只要在 data 標籤中定義變數即可
:::

## 之後可能要看的文章
[雙向綁定](https://medium.com/jastzeonic/android-data-binding-%E7%9A%84%E9%9B%99%E5%90%91%E7%B6%81%E5%AE%9A-1516383c4315)

https://connorlin.github.io/2016/07/02/Android-Data-Binding-%E7%B3%BB%E5%88%97-%E4%B8%80-%E8%AF%A6%E7%BB%86%E4%BB%8B%E7%BB%8D%E4%B8%8E%E4%BD%BF%E7%94%A8/

https://medium.com/jastzeonic/android-data-binding-%E7%9A%84-event-handling-97d5ca59e220

[V2](https://ithelp.ithome.com.tw/articles/10196991)

## 延伸閱讀
[Data Binding 在 xml 裡的各種語法](https://ithelp.ithome.com.tw/articles/10220748)

## 參考文章
[MVVM 架構](https://ithelp.ithome.com.tw/articles/10192829)
[Android 从观察者模式到 DataBinding](https://www.jianshu.com/p/b9af64c9fa04)
[Using DataBinding library for binding events](https://stackoverflow.com/questions/31961901/using-databinding-library-for-binding-events)

## 遇到的問題
Dialog 的處理
APIManager 回來的處理