###### 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 回來的處理