# Android筆記--DataBinding 前置閱讀: [Android筆記(kotlin)–ViewBinding(視圖綁定)](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_viewBinding) DataBinding 是 Android 提供的一個工具,可以讓 XML 中的 UI 元素直接綁定到程式碼中的資料物件。這能==大幅簡化 UI 的更新邏輯,減少繁瑣的 findViewById() 操作並且可以在資料變化時,UI 自動更新(尤其搭配 `LiveData` 或 `Observable`)==,提高程式碼的可讀性與維護性。 <u>**`DataBinding` 和 `ViewBinding` 在某些功能上有重疊,但它們的設計目標與使用場景不同**</u>: | 特性 | DataBinding | ViewBinding | | --- | --- | --- | | **目標** | 提供強大的資料與 UI 綁定功能,支援 MVVM 架構。 | 提供簡化的 `findViewById` 替代方案。 | | **資料綁定能力** | 支援資料綁定,直接將資料對應到 UI。 | 不支援資料綁定,只處理 XML 元素引用。 | | **雙向綁定** | 支援雙向資料綁定,適合用於表單輸入等情境。 | 不支援雙向綁定。 | | **語法** | 需要在 XML 中加入表達式(如 `@{}`)。 | 無需改動 XML,只生成與 XML 對應的綁定類。 | | **適用場景** | - 需要資料驅動 UI 時<br/>- 使用 MVVM 架構時 | - 僅需操作 View<br/>- 簡單 UI 互動場景 | 依賴: --- [※==參考==※](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_viewBinding?stext=962%3A5%3A0%3A1734060166%3Au9gAu7) 在app層級的`build.gradle.kts`中開啟即可(和ViewBinding一樣) ```kotlin= android { ... buildFeatures { dataBinding true } } //或是 android { ... dataBinding { enable = true } } ``` 簡單使用: --- 用DataBinding讓一個TextView和ViewModel關連並綁定,使VM內的值有變化時,TextView也會自動更新 - <u>**步驟1**</u><span id="portalA"></span>.到要使用DataBinding的布局文件中,對最外層的Layout按下`Alt+Enter`,選擇`convert to data binding layout`,系統就會將布局文件轉換成DataBinding的樣式。 | 轉換前 | 轉換後 | | -------- | -------- | | ![image](https://hackmd.io/_uploads/HyEiZWaEkl.png) | ![image](https://hackmd.io/_uploads/rkWGXZa4Jl.png)| - <u>**步驟2**</u>.在轉換後新出現的`<data>`區塊中新增`<variable>`,在`<variable>`中宣告要使用到的變數名、類別的全路徑,需填寫兩個值`name`和`type` - `name`: 這個class在這個 xml 的命名 - `type`: 這個class的所在位置,填上 class 完整的 package name,就能夠在這裡使用這個 class 的 public 變數 ```kotlin= <data> <variable name="myViewModel" type="com.example.mypratice_databinding_2.MyViewModel" /> </data> ``` ![image](https://hackmd.io/_uploads/SJzYubpVyg.png) - <u>**步驟3**</u>.將要使用DataBinding的地方,替換成`@{}`,內部呼叫要使用的變數,例如:`myViewModel的text_1` ```kotlin= android:text="@{myViewModel.text_1}" ``` ![image](https://hackmd.io/_uploads/S1X6dZp4kg.png) <u>**備註**</u>:使用`@{}`的View,在預覽視窗中文字會顯示空白,是正常現象,可以改用`android:text="@{myViewModel.text_1, default = XXX}"`,就會文字了 - <u>**步驟4**</u>.在要使用DataBinding的程式碼中聲明Binding及viewModel(如果有使用到VM的話) 聲明binding的方法和ViewBinding一樣,如果同時使用ViewBinding和DataBinding的話,只要聲明一次即可,不需要額外操作。 ```kotlin= binding = ActivityMainBinding.inflate(layoutInflater) viewModel = ViewModelProvider(this).get(MyViewModel::class.java) ``` - <u>**步驟5**</u>.將 ViewModel (viewModel) 綁定到 binding,以便在 XML 檔案中可以直接使用 ViewModel 的資料和方法 ```kotlin= binding.myViewModel = viewModel ``` ![image](https://hackmd.io/_uploads/r1sy1MaVkl.png) - <u>**步驟6**</u>.聲明`lifecycleOwner` `lifecycleOwner` 是為了讓資料繫結系統能感知 Activity 的生命週期,從而監聽 LiveData 的變化並自動更新 UI。 ```kotlin= //在Activity中聲明lifecycleOwner用this binding.lifecycleOwner = this //在fragment中聲明lifecycleOwner用viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner ``` <br/> <u>**至此,我們的佈局中的TextView已經能依照ViewModel中的值的變化自動更新了。**</u> [GitHub專案](##GitHUb專案:) 進階: --- ==警告: 過度或花式使用DataBinding可能會導致代碼可讀性降低、測試困難等後果== :::spoiler 取代`findViewByID()` 類似於[ViewBinding](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_viewBinding),DataBinding也可以取代`findViewByID()`來對視圖進行引用 - 步驟1.將布局文件轉換成DataBinding的樣式。[參考](#portalA) - 步驟2-1.在Activity裡,使用`DataBindingUtil.setContentView()` ```kotlin= //在Activity裡的話,使用 binding = DataBindingUtil.setContentView(requireActivity(), R.layout.fragment_bmi_history) ``` - 步驟2-2.在Fragment裡的話,和[viewBinding一樣](https://hackmd.io/VqYnjlp8R6qotXik1gUnPg?view#%E2%80%BB%E5%9C%A8Fragment%E4%B8%AD%E4%BD%BF%E7%94%A8ViewBinding%E2%80%BB)使用`binding.inflate()` ```kotlin= //範例 class BmiHistory : Fragment() { private lateinit var binding: FragmentBmiHistoryBinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { binding = FragmentBmiHistoryBinding.inflate(inflater, container, false) return binding.root } } ``` ::: :::spoiler 雙向綁定: 資料從VM流向UI,同時,資料從 UI 流向 ViewModel,即 ViewModel ↔ UI。 - 基本版的DataBinding我們使用的`@{}`是單向綁定,也就是VM變時UI會跟著更新,但是反過來,UI變時VM中的值不會跟著更新,是單向的。 - 雙向綁定則是使用`@={}`,當VM變時UI會跟著更新,且UI更新時VM也會跟著跟新,是雙向的。<br/>例如:當使用者在 EditText 中輸入內容,例如輸入 World,viewModel.editText 的值會自動更新為 World。 - 適合用在表單輸入、搜尋框等需要即時回傳使用者輸入的場景。 ```xml= <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@={viewModel.editText}" /> ``` ::: :::spoiler 格式化輸出: 在使用DataBinding時使用反引號「\`」進行字串模板格式化輸出,透過反引號來建立字串模板。 例如說,我們想要輸出「Name: AAA」,並且AAA使用dataBinding連結ViewModel的話,就要這樣寫: ```kotlin= //A寫法 android:text="@{`Name: ${myViewModel.name}`}" //B寫法 android:text="@{`Name:` + myViewModel.name}" ``` ::: :::spoiler 將方法用DataBinding綁定 - 直接綁定: 當按鈕被點擊時,直接呼叫ViewModel中的onButtonClick()方法 `android:onClick="@{() -> viewModel.onButtonClick()}"` - 帶參數的方法綁定: 當EditText文字變更時,呼叫ViewModel中的onTextChanged()方法,並傳遞文字參數 `android:onTextChanged="@{(text, start, before, count) -> viewModel.onTextChanged(text)}"` ```kotlin= //在XML中 <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="點擊我" android:onClick="@{() -> viewModel.onButtonClick()}" /> <!-- 單向綁定:監聽文字變更 --> <!-- 備註:這裡只是示範多參數傳遞的方法,實際上用到的參數只有text一個 --> <EditText android:id="@+id/singleWayBindingEditText" android:layout_width="match_parent" android:layout_height="wrap_content" android:onTextChanged="@{(text, start, before, count) -> viewModel.onTextChanged(text)}" android:hint="單向綁定" /> <!-- 雙向綁定 --> <EditText android:id="@+id/twoWayBindingEditText" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@={viewModel.inputText}" android:hint="雙向綁定" /> ``` ```kotlin= //在ViewModel中 class MyViewModel2:ViewModel() { // 單向綁定的消息 private val _message = MutableLiveData<String>() val message: LiveData<String> = _message fun onButtonClick() { // 按鈕點擊事件可以同時修改兩種方式的值 _message.value = "按鈕已被點擊!" } fun onTextChanged(text: CharSequence?) { // 單向綁定的文字變更處理 _message.value = "單向綁定文字: ${text.toString()}" } // 雙向綁定的文本 val inputText = MutableLiveData<String>().apply { value = "" // 初始值 } } ``` ```kotlin= //在Activity中 viewModel.inputText.observe(this) { text -> Toast.makeText(this, "當前文字: $text", Toast.LENGTH_SHORT).show() } ``` ::: :::spoiler 串接xml字串資源 以下將示範四種串接法 - 前置 ```kotlin= // User.kt (數據模型) data class User( val name: String, val age: Int, val points: Int ) // ViewModel (MainViewModel.kt) class MainViewModel : ViewModel() { val user = User( name = "張三", age = 30, points = 150 ) } ``` - 基本字串插值 ```kotlin= //資源 <resources> <string name="welcome_message">歡迎, %1$s!</string> </resources> //布局文件 <!-- 基本字串資源 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{@string/welcome_message(user.name)}" /> //輸出結果 //歡迎, 張三! ``` - 多參數字串 ```kotlin= <resources> <string name="user_info">姓名: %1$s, 年齡: %2$d</string> <resources> <!-- 多參數字串資源 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{@string/user_info(user.name, user.age)}" /> //輸出結果 //姓名: 張三, 年齡: 30 ``` - 數字格式化 ```kotlin= <resources> <string name="points_format">目前積分: %1$d 分</string> <resources> <!-- 帶數字的字串資源 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{@string/points_format(user.points)}" /> //輸出結果 //目前積分: 150 分 ``` - 條件字串顯示 ```kotlin= <resources> <string name="high_points">積分達標,獲得獎勵!</string> <string name="low_points">繼續加油,積分有待提升</string> <resources> <!-- 條件顯示字串資源 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.points > 100 ? @string/high_points : @string/low_points}" /> //輸出結果(當前積分150,大於設定值100,所以顯示@string/high_points) //積分達標,獲得獎勵! ``` ::: :::spoiler 型別轉換 - 數字型別轉換為字串: 使用 String.valueOf() 將數字轉換為字串 ```kotlin= android:text="@{String.valueOf(user.id)}" android:text="@{String.valueOf(user.age)}" ``` - 浮點數格式化: 使用 String.format() 控制小數點位數,(.1f 表示保留一位小數) ```kotlin= android:text="@{String.format(`身高: %.1f 公分`, user.height)}" ``` - 自定義型別轉換: 在ViewModel中定義轉換方法 ```kotlin= //根據年齡劃分不同階段 // 定義數據模型 data class User( val id: Long, val name: String, val age: Int, val height: Float, val isActive: Boolean ) // ViewModel (MainViewModel.kt) class MainViewModel : ViewModel() { // 自定義型別轉換方法 fun convertAgeToLevel(age: Int): String { return when { age < 18 -> "青少年" age in 18..35 -> "青年" age in 36..55 -> "中年" else -> "老年" } } } <!-- 自定義型別轉換 --> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{viewModel.convertAgeToLevel(user.age)}" /> ``` ::: :::spoiler 布林值轉換: 使用三元運算符根據布林值顯示不同文字 ```kotlin= android:text="@{user.isActive ? `使用者已啟用` : `使用者未啟用`}" //等同於 if (user.isActive) { "使用者已啟用" } else { "使用者未啟用" } ``` ::: :::spoiler 空值處理 ```kotlin= android:text="@{user.name ?? `未知使用者`}" //user.name 是非 null,則使用 user.name 的值 //如果 user.name 是 null,則使用 ?? 後面的值(即 未知使用者)。 ``` ::: Q&A --- <font size=4>**1.如果要在Fragment中使用DataBinding,應該在Fragment還是Activity的Layout加上[<data>...</data>](#portalA)?</font>** :::spoiler 解答 如果要在Fragment中使用DataBinding,那應該要在Fragment的Layout中將佈局文件修該為`<data></data>`而不是修改所依附Activity的佈局文件,因為Fragment 的 View 可以在 onDestroyView() 時被銷毀,但 Activity 的 View 不會,這會導致潛在的 Memory Leak。 ::: 踩坑紀錄 --- ### Q:如果在把Databinding綁定的檔案移動到新的 package 中,並在 XML 中使用 DataBinding 綁定時即使提供了正確的路徑可能會出現錯誤 例如,當我們在XML檔案中進行以下綁定 ```xml= <data> <variable name="viewModel" type="com.example.task_15.task_fragment.TaskViewModel" /> </data> ``` 在這個情況下,如果出現了下列兩種錯誤: ``` //錯誤1 e:Cannot access class 'TaskFragment.TaskViewModel'. Check your module classpath for missing or conflicting dependencies. //錯誤2 e: Assignment type mismatch: actual type is 'com.example.task_15.TaskFragment.TaskViewModel', but 'TaskFragment.TaskViewModel?' was expected. ``` 這時,「「請檢查Package的命名是否為全小寫」」,將package的名稱改為小寫即可解決,==因為如果包名中包含大寫字母,可能會導致 DataBinding 無法正確生成綁定類。== ![image](https://hackmd.io/_uploads/HJ-JwO1q1x.png) GitHUb專案: --- - 基本版:[MyPratice_Databinding_1](https://github.com/PudCheetah/MyPratice_Databinding_1) - 多重參數傳遞:[MyPratice_Databinding_3](https://github.com/PudCheetah/MyPratice_Databinding_3) 參考資料: --- - [AndroidDeveloper--数据绑定库](https://developer.android.com/topic/libraries/data-binding?hl=zh-cn) - [AndroidDeveloper--Android 中的数据绑定](https://developers.google.com/codelabs/android-databinding?hl=zh-cn#0) - ★ [Android DataBinding 从入门到进阶](https://juejin.cn/post/6844903609079971854) - [YT--DataBinding in Android](https://youtu.be/h135uamWlbg?si=19slLAU075AwUX2c) - [Day 8 Data Binding (一) 介紹與基本使用](https://ithelp.ithome.com.tw/m/articles/10219729) - [Kotlin + Android Databinding 那一兩件事](https://medium.com/jastzeonic/kotlin-android-databinding-%E9%82%A3%E4%B8%80%E5%85%A9%E4%BB%B6%E4%BA%8B-5d223466615a) - [Data Binding Library 實作 MVVM Pattern 那兩三件事情](https://medium.com/jastzeonic/data-binding-library-%E5%AF%A6%E4%BD%9C-mvvm-pattern-%E9%82%A3%E5%85%A9%E4%B8%89%E4%BB%B6%E4%BA%8B%E6%83%85-1d6e0de66aaf) - ★ [DataBinding,让人又爱又恨的一个框架](https://juejin.cn/post/6857815150565687303) - [犹豫要不要用DataBinding?这篇文章帮你解惑](https://blog.csdn.net/c10WTiybQ1Ye3/article/details/106934714)