之前我們做的都是 Data Binding 的單向綁定,ViewModel 一有變化就去通知 View 做更新,但是 View 有變化卻不會去通知 ViewModel(Ex.使用者在輸入框輸入文字)。
舉例來說:
<EditText
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="@{viewModel.content}" />
<TextView
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="@{viewModel.content}" />
有一個 EditText
和 TextView
他們的 text 都是去抓 viewModel.content
但是我在 EditText
打字時卻不會去更新 viewModel.content
做改變,TextView
就不會更新。
要完成雙向綁定其實很簡單,只要把 EditText
改成
<EditText
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="@={viewModel.content}" />
在大括號前面加一個等於就可以了,現在只要 EditText
的 text 一有變化就會去通知 viewModel.content
做改變,而 viewModel.content
一改變,ViewModel 就會去通知 TextView
更新,達到 EditText
和 TextView
同步的效果。
當然不會所有事都這麼簡單,這邊就來做一個客製化的雙向綁定。
這邊有兩個 Spinner,第二個 Spinner 的內容會跟著第一個 Spinner 的選擇做改變
@BindingAdapter("spinnerList")
fun setSpinnerList(spinner: Spinner, contentList: ObservableField<List<String>>) {
val adapter = ArrayAdapter(
spinner.context,
android.R.layout.simple_spinner_dropdown_item,
contentList.get()!!
)
spinner.adapter = adapter
}
@BindingAdapter("selectionPosition")
fun setSpinnerSelectionPosition(spinner: Spinner, selectPosition: Int) {
spinner.setSelection(selectPosition)
}
這邊和一般的綁定一樣,用自定義的 BindingAdapter 把 List 丟給 Spinner,
只是第一個 Spinner 的選擇要跟 ViewModel 雙向綁定,用雙向綁定就會需要一個 getter
這時候可以用 @InverseBindingAdapter
來完成這件事
@InverseBindingAdapter(attribute = "selectionPosition")
fun getSpinnerSelectionPosition(spinner: Spinner): Int {
return spinner.selectedItemPosition
}
這裡的 attribute 要跟 setter 的 key 對應,這時候 Databinding 會產生一個 {Attribute name}+AttrChanged 的 BindingAdapter value 來獲取數據改變的時機,因為這是 Spinner,所以就在 onItemSelected
裡面去呼叫onChange()
@BindingAdapter("selectionPositionAttrChanged")
fun setSpinnerOnChangeListener(spinner: Spinner, listener: InverseBindingListener) {
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
listener.onChange()
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
最後再把 Spinner 與 ViewModel 綁定,記得 selectionPosition 的 @
後面要加上 =
<Spinner
selectionPosition="@={viewModel.selectedPosition}"
spinnerList="@{viewModel.parentList}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Spinner
spinnerList="@{viewModel.subList}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp" />
這樣就大功告成囉,附上完整程式碼
Data Binding 算是一個全新的概念,把先前寫在 Java 檔的東西都搬到了 xml 去做,一開始在玩的時候覺得真的蠻酷的,但是要去做一些比較複雜的事情時才開始體會到 Data Binding 的坑真的蠻多的,像是
要 debug 時,Databinding 顯示的錯誤 Log 不會告訴我是哪一行,必須要自己去檢查,甚至還有這種超出版面的 Log
得要整串複製下來貼在別的地方才看得到,蠻不方便的
沒有 AutoComplete (自動補全),在 xml 的程式碼幾乎都要每個字自己打出來,不像 Java 檔一個單字打前面一兩個字母就會有很多建議選單跑出來,所以常常打錯一個字母就要找錯找很久,無形中也浪費了很多 debug 的時間。
雖然坑很多,但是只用一些基本的功能還是挺方便的,而且這算是蠻新的組件,也期許谷歌把拔可以再對 Data Binding 做一些優化,把 Data Binding 提升到另一個境界。
有任何問題或講得不清楚的地方歡迎留言和我討論。
更歡迎留言糾正我任何說錯的地方!
下一篇:LiveData 介紹與使用