圖片來自必應
Android-Architecture-Components官方文件
Databinding是Google推出的一個支援View與ViewModel繫結的Library,可以說Databinding建立了一個UI與資料模型之間的橋樑,即UI的變化可以通知到ViewModel, ViewModel的變化同樣能夠通知到UI從而使UI發生改變,大大減少了之前View與Model之間的膠水程式碼,如findViewById, 改變及獲取TextView的內容還需要呼叫setText()、 getText(),獲取EditText編輯之後的內容需要呼叫getText(),而有了Databinding的雙向繫結,這些重複的工作都將被省去。下面我們就來看一下如何使用Databinding來雙向繫結
首先我們先定一個ViewModel,將這個ViewModel的變數content與佈局檔案中的TextView繫結:
class MyViewModel: ViewModel(){
var content: ObservableFiled<String> = ObservableFiled()
}
複製程式碼
- 官方支援的雙向繫結 這裡的官方支援指的是Databinding庫中已經定義了一些View的雙向繫結,我們如果要使用的話只需要將xml檔案中的"@{}"改成"@={}", 如下程式碼
(test_layout.xml):
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="model"
type="com.fb.onedayimprove.viewmodel.MyModel"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{model.content}"
/>
<!--android:text="@={model.content}"-->
</LinearLayout>
</layout>
複製程式碼
上述程式碼中註釋部分就是將單向繫結改為雙向繫結的程式碼
官方支援的雙向繫結:
- AbsListView android:selectedItemPosition
- CalendarView android:date
- CompoundButton android:checked
- DatePicker android:year, android:month, android:day
- NumberPicker android:value
- RadioGroup android:checkedButton
- RatingBar android:rating
- SeekBar android:progress
- TabHost android:currentTab
- TextView android:text
- TimePicker android:hour, android:minute
那麼雙向繫結是怎麼實現的呢?首先來看一下Databinding的原始碼:
首先是我們在編譯之後會生成幾個相關檔案如test_layout.xml, test_layout-layout.xml, TestLayoutBinding.java, BR檔案等。我們主要來看一下TestLayoutBinding.java這個檔案。這個檔案的主要作用是宣告xml內的View控制元件以及宣告的ViewModel,如本例中宣告瞭一個TextView:
@NonNull
private final android.widget.LinearLayout mboundView0;
@NonNull
private final android.widget.TextView mboundView1;
// variables
@Nullable
private com.fb.onedayimprove.viewmodel.MyModel mModel;
複製程式碼
上述程式碼中mboundView0為根佈局、mboundView1為宣告瞭雙向繫結的TextView,mModel為與View繫結的ViewModel。(注意:並不是所有的變數都會被宣告,有三種情況:在xml佈局中宣告id的,將會被定義為靜態變數,可以直接通過Databinding物件訪問;引用了model資料的;根佈局)
接下來當我們定義了雙向繫結的時候,TestLayoutBinding.java會生成這樣一段程式碼:
private android.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new android.databinding.InverseBindingListener() {
@Override
public void onChange() {
// Inverse of model.content.get()
// is model.content.set((java.lang.String) callbackArg_0)
//呼叫定義的TextViewBindingAdapter中的getTextString(TextView view)方法得到mboundView1(即佈局中定義的TextView)的值
java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
// localize variables for thread safety
// model
com.fb.onedayimprove.viewmodel.MyModel model = mModel;
// model.content.get()
//當前View繫結的ViewModel中的content的值
java.lang.String modelContentGet = null;
// model != null
boolean modelJavaLangObjectNull = false;
// model.content
android.databinding.ObservableField<java.lang.String> modelContent = null;
// model.content != null
boolean modelContentJavaLangObjectNull = false;
modelJavaLangObjectNull = (model) != (null);
if (modelJavaLangObjectNull) {
modelContent = model.getContent();
modelContentJavaLangObjectNull = (modelContent) != (null);
if (modelContentJavaLangObjectNull) {
//將View中的值取出並複製給相應的model中繫結的資料
modelContent.set(((java.lang.String) (callbackArg_0)));
}
}
}
};
複製程式碼
通過上述程式碼可以看到實現逆向繫結的關鍵部分,可以看到呼叫InverseBindingListener介面的onChange()方法就可以將view的值傳回ViewModel。那麼它在哪裡被用到呢?看下面的程式碼:
@Override
protected void executeBindings() {
...
if ((dirtyFlags & 0x4L) != 0) {
android.databinding.adapters.TextViewBindingAdapter.setTextWatcher(..., mboundView1androidTextAttrChanged);
}
}
複製程式碼
可以看到在executeBindings這個方法中,將InverseBindingListener 的值傳給setTextWatcher,這個方法怎麼來的後面會提到。
上面的程式碼中提到一個TextViewBindingAdapter,通過它的名字就可以看出是繫結View與ViewModel的介面卡,那麼來看一下TextViewBindingAdapter做了什麼 (在包android.databinding.adapters下):
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
//為防止雙向繫結無限迴圈呼叫比較新舊內容是否相同
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
複製程式碼
上述程式碼相信大家並不陌生,當Databinding在給某一個控制元件的XXX屬性賦值的時候,需要去找到相應的setXXX()方法,然後將model中的值給這個View。而@BindingAdapter ("xxx")正是將這個setXXX()方法與xxx屬性關聯起來,所以上面部分的程式碼作用總結起來就是做了view.setText(text)。(注:可以看到程式碼中有對新舊內容的比較,只有當內容不同的時候才會執行下一步,這是為了防止雙向繫結迴圈呼叫)
再來看下一個方法:
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
複製程式碼
這個方法在TestLayoutBinding.java中已經說明了,是雙向繫結取值時呼叫的方法,@InverseBindingAdapter註解和@BindingAdapter註解作用類似。
再來看下一個關鍵方法:
@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
"android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
final OnTextChanged on, final AfterTextChanged after,
final InverseBindingListener textAttrChanged) {
final TextWatcher newValue;
//listener為空則不作處理
if (before == null && after == null && on == null && textAttrChanged == null) {
newValue = null;
} else {
//建立一個TextWatcher監聽text內容的變化
newValue = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (before != null) {
before.beforeTextChanged(s, start, count, after);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (on != null) {
on.onTextChanged(s, start, before, count);
}
//如果設定了雙向繫結,則InverseBindingListener不為空,可參見之前的TestLayoutBinding.java對其的賦值。
if (textAttrChanged != null) {
//呼叫onChange()方法(實現在TestLayoutBinding.java中),即將view的值傳回給ViewModel
textAttrChanged.onChange();
}
}
@Override
public void afterTextChanged(Editable s) {
if (after != null) {
after.afterTextChanged(s);
}
}
};
}
final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
if (oldValue != null) {
view.removeTextChangedListener(oldValue);
}
if (newValue != null) {
view.addTextChangedListener(newValue);
}
}
複製程式碼
從上面的程式碼我們可以看到@BindingAdapter繫結了幾個屬性,其中有一個叫做"android:textAttrChanged",那麼這個屬性就代表了當TextView的text屬性變化,xxxAttrChanged。可以想象得到那這個方法就是在android:text屬性發生變化的時候會被呼叫。我們在這個方法中設定了對android:text這個屬性內容變化的監聽(注意是內容變化,不是屬性變化),以後當每次改內容變化的時候,就會呼叫textAttrChanged.onChange()這個方法將TextView的值傳回給ViewModel。
上述的是通過官方支援的原始碼來看雙向繫結,那麼我們要自定義的View使用雙向繫結應該怎麼做呢?
-
自定義View雙向繫結
通過對官方原始碼分析,我們發現要實現雙向繫結關鍵是需要實現Adapter,實現setter,getter,以及Listene方法。下面就讓我們來實現一個自定義View 的雙向繫結吧。 對MyModel稍作修改:
class MyModel: ViewModel(){
var content: ObservableField<String> = ObservableField()
fun onChangeClick(view: View){
if(view is TestEditView){
Log.v("---TAG---", content.get())
view.setValue("改變後")
Log.v("---TAG---", "click: ${content.get()}")
}
}
}
複製程式碼
對test_layout.xml修改(主要是使用自定義的View),加了一個點選事件,點選之後修改View的內容:
<com.fb.onedayimprove.widget.TestEditView
android:layout_width="match_parent"
android:layout_height="50dp"
app:content="@={model.content}"
android:onClick="@{(view) -> model.onChangeClick(view)}"
/>
複製程式碼
這裡我們引入一個自定義View:這是一個類似表單的View,左邊是個小標題,右邊可以填一些資訊等:
class TestEditView: LinearLayout{
private lateinit var contentView: TextView
private var onContentedListener: OnContentChangedListener? = null
{
...
}
private fun initView(context: Context){
val view = View.inflate(context, R.layout.test_edit_layout, this)
contentView = view.findViewById(R.id.text_content)
}
fun setOnContentListener(
listener: OnContentChangedListener?){
this.onContentedListener = listener
}
public fun setValue(content: String){
if (content.isEmpty() || (!content.isEmpty() && content == contentView.text)){
return
}
contentView.text = content
onContentedListener?.onContentChanged()
}
fun getContent(): String{
return contentView.text.toString()
}
//內容改變Listener
interface OnContentChangedListener{
fun onContentChanged()
}
}
複製程式碼
省略了部分程式碼,主要就是定義了一個內容改變的Listener。 下面是TestEditViewAdapter的程式碼:
//使用InverbaseBindingMethod註解,與使用@BindingAdapter效果一樣其中,event、method可宣告也可以不宣告,不宣告的話會預設設定xxxAttrChanged 與 getXXX() 方法
//@InverseBindingMethods(InverseBindingMethod(type = TestEditView::class, attribute = "content", event = "contentAttrChanged", method = "getContent"))
object TestEditViewAdapter{
@JvmStatic
@BindingAdapter("app:content")
fun setContent(view: TestEditView, content: String){
if (content.isNotEmpty() && view.getContent() == content){
return
}
view.setValue(content)
}
@JvmStatic
@InverseBindingAdapter(attribute = "app:content", event = "contentAttrChanged")
fun getContent(view: TestEditView): String{
return view.getContent()
}
@JvmStatic
@BindingAdapter(value = "app:contentAttrChanged", requireAll = false)
fun setContentAttrChangedListener(view: TestEditView, bindingListener: InverseBindingListener){
if(bindingListener == null){
view.setOnContentListener(null)
}else{
//如果設定了雙向繫結則為其新增監聽
view.setOnContentListener(object : OnContentChangedListener{
override fun onContentChanged() {
bindingListener.onChange()
}
})
}
}
}
複製程式碼
接下來我們只需要在Fragment中為content賦值,然後執行程式,點選TestEditView,就可以看到Log輸出了content的初始值與改變後的值(不貼圖展示了)。
val binding = TestLayoutBinding.inflate(inflater!!, container, false)
val model = MyModel()
model.content.set("初始值")
binding.model = model
return binding.root
複製程式碼
-
總結 以上就是Databinding雙向繫結的使用方法,總的來說應該注意兩點:
1、修改"@{}" 為 "@={}"
2、寫setXXX(), xxxAttrChanged(), getXXX()方法。