如何高效地實現以下界面?
登錄/未登錄
有好幾年findViewById實戰經驗的我,感覺並不難啊。一般會
1.先定義一個User的Model類,數據來自JSON解析;
2.創建一個xml,隨後在xml中布局完所有View,對頭像、標題、積分、登錄按鈕一個id;
3.在Activity中通過findViewById獲取到頭像ImageView、標題TextView、積分TextView、登錄Button,然後給Button設置監聽器,再根據登陸狀態展示對應數據;
實現如下:
User.java
activity_detail.xml
DetailActivity
可以看到,在Activity中View的定義、find、判空佔據了大量篇幅,我們需要更優雅的實現。
2.1 ButterKnife你可能聽說過Jake Wharton的ButterKnife,這個庫只需要在定義View變量的時候通過註解傳入對應id,隨後在onCreate時調用ButterKnife.bind(this)即可完成view的注入,示例如下:
2.2 Android Data Binding如果使用了Android Data Binding,那麼View的定義、find、判空這些都不用寫了,如何做呢?
2.2.1 準備工作首先,你需要滿足一個條件:你的Android Plugin for Gradle版本必須等於或高於1.5.0-alpha1版本,這個版本位於根目錄build.gradle中,示例如下:
接著,你必須告訴編譯器開啟Data Binding,一般位於app:build.gradle的android標籤中,示例如下:
2.2.2 修改layout.xml以activity_detail.xml為例,原來的根節點為LinearLayout,如下所示:
我們拷一份activity_detail.xml,改為activity_detail2.xml,並且需要在外面wrap一層layout標籤,修改後的activity_detail2.xml為:
2.2.3 開始享受樂趣吧!在上述操作完成後,編譯器會自動為我們生成
com.asha.demo.databinding.ActivityDetail2Binding.java類,這個類的命令方式為:包名 + databinding + activity_detail2駝峰命名方式 + Binding.java。隨後,使用這個activity_detail2的DetailActivity2.java的代碼可以簡化為:
是的,所有View的定義、find、判空都不見了,所有的這些操作都在編譯器為我們生成的ActivityDetail2Binding.java中完成,只需要在onCreate時調用如下代碼進行setContentView即可實現,
我的天哪
2.2.4 ActivityDetail2Binding中注入View相關的代碼分析可以在as中方便的查看編譯器自動生成的類,這個類位於/app/build/intermediates/classes/debug/com/asha/demo/databinding/ActivityDetail2Binding.class中,縮減掉Binding邏輯後的代碼為:
其中全局靜態SparseIntArray數組中存放了4個數字,這個四個數字為R.java中生成的對應View的id,
在ActvityDetail2Binding實例構造的時候調用了mapBindings,一次解決了所有View的查找,mapBindings函數在ActvityDetail2Binding父類ViewDataBinding中實現。
3 使用表達式在layout.xml中填充model數據在ActivityDetail2.java中還存在大量的View控制、數據填充代碼,如何把這些代碼在交給layout.xml完成呢?
3.1 ModelAdapter類第2節中已經定義了User.java類作為Model類,但是我們經常會遇到Model類和真正View展示不一致的情況,本例子中定義一個來ModelAdapter類來完整Model數據到展示數據的適配。示例代碼為ActivityDetail3.java的內部類,可以調用ActivityDetail3.java中的函數,代碼定義如下:
3.2 activity_detail3.xml中使用model同樣複製一份activity_detail2.xml為activity_detail3.xml,在<layout>節點加入<data>節點,並且在裡面定義需要用的model類(比如ModelAdapter adapter),當然也可以是基礎類型變量(比如int visibility);
隨後,就可以在下面的view中使用表達式了,全部布局文件如下:
3.3 DetailActivity3.java中調用填充如下代碼所示,只需要在登錄狀態改變的時候,給viewDataBinding設置所需要的adatper、visibility值,即可完成數據的填充
3.4 ActivityDetail3Binding中填充相關的代碼分析同樣,ActivityDetail3Binding中,編譯器根據activity_detail3.xml中的<data>標籤,自動生成了諸如setAdapter、setVisibility的代碼,setAdapter相關代碼如下:
非常簡單,自動生成了getter和setter,在完成set操作後,調用執行notifyPropertyChanged和super.requestRebind()
notifyPropertyChanged
ViewDataBinding本身就是一個BaseObservable, 在往ViewDataBinding註冊觀察某個屬性的變化,如果註冊了mAdapter的變化,對應的觀察器就會接收到回調。相關邏輯與反向Binding相關,谷歌官方還沒給出相關使用文檔,不再深入分析;
super.requestRebind()
1.此函數為ViewDataBinding中的函數,具體實現為判斷現在是否有Rebind請求,如果有則return;如果沒有則根據運行時sdk版本交給handler或者choreographer插入到下一幀中執行mRebindRunnable。
2.在mRebindRunnable中會根據當前sdk版本,如果大於等於KITKAT,則需要在onAttachToWindow後執行executePendingBindings;否則直接執行executePendingBindings。
3.在父類ViewDataBinding中經過一些的判斷,調用到ActivityDetail3Binding中的executeBindings,在executeBindings中根據dirtyFlags執行不同的View屬性賦值,以下所有ActivityDetail3Binding相關代碼都是編譯器自動生成的至此,完成了View數據的填充分析。
4 Binding自動生成的ViewDataBinding類(例如ActivityDetail3Binding)內包含了Model + View,是MVVM中的MV的概念。
第2章的View注入,第3章的View賦值都是鋪墊,他們最後都是為Binding操作進行服務。目前谷歌已經支持雙向Binding,但上文已經提到,目前資料比較少。本文只關注單向的Binding,即:Model的變化,自動同步到View上。
4.1 使用ObservableField目前所提供的ObservableField有:
Observable類型
對應原類型
ObservableArrayList
ArrayList
ObservableArrayMap
ArrayMap
ObservableBoolean
boolean
ObservableByte
byte
ObservableChar
char
ObservableFloat
float
ObservableDouble
double
ObservableLong
long
ObservableInt
int
ObservableParcelable<T extends Parcelable>
<T extends Parcelable>
ObservableField<T>
<T>
本文使用簡單的ObservableInt作為示例,解決visibility的單項綁定問題。
改造activity_detail4.xml:定義類型為ObservableInt的variable,name為visibility,隨後賦值給ImageView的android:visibility,示例如下:
改造DetailActivity4.java,只需要在onCreate時把visibility賦值給binding(ActivityDetail4Binding)即可,後面對visibility的操作,就會更新到view上,示例代碼如下:
4.2 ActivityDetail4Binding中單向綁定相關的代碼分析與給ActivityDetail4Binding直接set純Model不同,所有的ObservableField都實現了Observable接口,只要實現了Observable接口,都是單向Binding類型,所以ActivityDetail4Binding中的setVisibility多加了一行代碼:this.updateRegistration(1, visibility),其中1為propertyId,目前一共自動生成了2個,0為adatper,1為visibility,代碼如下:
updateRegistration函數為ViewDataBinding中的函數,會根據 Observable、ObservableList、ObservableMap三種類型,分別創建對應的Listener。ObservableInt為Observable,所以會使用CREATE_PROPERTY_LISTENER,在registerTo函數中創建WeakPropertyListener,
代碼如下:
在WeakPropertyListener的mListener有個setTarget函數,這個函數會向mObservable(即外面傳進來的visibility)註冊一個監聽器,如果visibility值發生變化,這個listener就會得到通知,回調到WeakPropertyListener的onPropertyChanged,接著通知到binding(ActivityDetail4Binding)的handleFieldChange,在handleFieldChange中調用了ActivityDetail4Binding的onFieldChange函數,如果返回值為true,則在handleFieldChange中調用requestRebind(),通知View進行賦值更新界面,onFieldChange相關代碼如下:
4.3 Observable Objects與4.1 ObservableField類似,可以改造一下ModelAdapter:為getter方法增加@Bindable註解,為setter方法增加notifyPropertyChanged(com.asha.demo.BR.name)通知。其中,BR是根據@Bindalbe自動生成的類,給getter方法增加@Bindable註解後,BR文件自動會生成一個整型的name。改造後代碼如下:
隨後,在DetailActivity4.java中調用測試代碼,執行完會在1秒後改變adapter上的name值,並且同步到View上,測試代碼如下:
具體原理與4.1類似,不再贅述。
5 layout.xml中View屬性的setter在下述示例中,detail_name這個TextView想把adapter.name賦值給自身的text屬性,就需要調用textView.setText(String)方法,這個方法就是View屬性的setter方法。
5.1 @BindingAdapter上述的setter方法,Data Binding庫幫我們實現了大部分默認方法,具體方法參見android.databinding.adapters包下的類,下圖為ViewBindingAdatper具體實現,
ViewBindingAdatper
其中setter方法都為static方法,第一個參數都為自身的實例,後面為xml中傳入的參數,只要加入@BindingAdapter註解,編譯器就會全局搜索保存在一個temp文件中,並在生成類似ActivityDetail4Binding過程中去查找所需的setter方法的。如果需要自定義,只需要在任意app代碼中定義@BindingAdapter即可,例如:
5.2 DataBindingComponent很多情況下只是某個Binding文件(例如ActivityDetail4Binding)需要自定義setter方法,這個時候就需要使用DataBindingComponent,
完成後,這個ActivityDetail4Binding範圍內的所有android:alpha="@{foo}"的方式賦值alpha的setter函數都會使用MyComponent#setAlpha。
5.3 @BindingConversion有時候會遇到類型不匹配的問題,比如R.color.white是int,但是通過Data Binding賦值給android:background屬性後,需要把int轉換為ColorDrawable,實現方式如下:
對應在ActivityDetail4Binding.java中生成的代碼如下所示,其中AvatarAdapterObjectn1為int類型:
5.4 @BindingMethod例如layout.xml中android:onClick屬性,在Binding中真正使用setter時,就對應到了setOnClickListener方法,
6 Data Binding利用編譯器在背後做的那些事兒Data Binding相關的jar包由四部分組成,
1.baseLibrary-2.1.0-rc1.jar
作為運行時類庫被打進APK中;
2.DataBinderPlugin(gradle plugin)
在編譯期使用,利用gradle-api(之前叫transform-api,1.5生,2.0改名)處理xml文件,生成DataBindingInfo.java;
3.compiler-2.1.0-rc1.jar
在編譯器使用,入口類繼承自AbstractProcessor,用於處理註解,並生成Binding類,DataBindingCompoent.java,DataBinderMapper.java類;
4.compilerCommon-2.1.0-rc1.jar
被DataBinderPlugin和compiler-2.1.0-rc1.jar所依賴
為了提高運行時的效率,Data Binding在背後做了非常多的工作,下圖是我整理的編譯流程,如圖所示:
Data Binding編譯流程
6.1 相關對象介紹白色部分為輸入,包括
1.res/layout;
2.原始碼中的註解;
黃色部分為編譯器處理類,包括
1.aapt編譯時處理,入口類名為MakeCopy.java;
2.gradle-api處理,入口類名為DataBinderPlugin.java;
3.AbstractProcessor處理,入口類名為ProcessDataBinding.java;
藍色部分為中間產物,包括
1.data-binding-info文件夾,包含了layout的基本信息,導入的變量,View標籤中的表達式,標籤的位置索引等等,如下所示為data-binding-info/activity_detail3-layout.xml:
2.setter_store.bin,包含所有setter相關信息;
3.layoutinfo.bin,包含所有layout相關信息;
4.br.bin,包含所有BR相關信息;
以上bin文件都以Serializable方式序列化到磁碟上,需要的時候進行反序列化操作;
綠色部分為最終產物,包括
1.data-binding-layout-out(最終輸出到res/layout),即去掉根節點<layout>,去掉節點<data>,與不使用Data Binding時的layout相一致,例如data-binding-layout-out/activity_detail2.xml:
2.DataBindingInfo.class,一個看似空的類,但在SOURCE階段包含了一個@BindingBuildInfo註解,包含了基本DataBinding的基本信息,代碼如下:
3.DataBindingComponent.class,會根據自定義的DataBindingComponent自動生成對應實例化方法,例如:
4.ViewDataBinding.class的子類(ActivityDetail2Binding.class等)
5.BR.class,Bindable屬性索引表,例如:
6.DataBindingMapper.class,Mapper,用於尋找某個layout.xml對應的ViewDataBinding類,例如:
6.2 相關編譯流程STEP1 資源處理
aapt或者gradle執行時,都會觸發資源處理,在資源處理過程中,DataBinding都會掃描一遍現有的資源,生成不包含<layout>的data-binding-layout-out以及DataBinding所需要的data-binding-info;
STEP2 DataBindingInfo.class生成
在完成資源處理後,aapt或者gradle-api都會去執行DataBindingInfo.class生成操作,把相關的信息寫入DataBindingInfo.class的@BindingBuildInfo註解中;
STEP3 監聽到註解變化
生成@BindingBuildInfo註解,或者code中發現有新的註解寫入,AbstractProcessor註解處理器就開始執行註解處理。DataBinding中有一個ProcessDataBinding.java類專門來處理DataBinding相關的註解;
STEP4 ProcessDataBinding處理註解,生成bin
ProcessDataBinding中處理註解永遠會按順執行3步,ProcessMethodAdapter,ProcessExpressions,ProcessBindable。每次執行都會從磁碟反序列化對應的bin文件,然後忘bin中寫入新的,完成後再序列化到磁碟;
STEP5 生成最終產物
執行ProcessMethodAdapter生成DataBindingComponents.class;執行ProcessExpressions生成ViewDataBinding.class子類(ActivityDetail2Binding.class),並觸發DataBindingMapper.class更新;執行ProcessBindable生成BR.class,並觸發DataBindingMapper.class更新;
第二章有講到View是如何注入的,其實需要分兩種情況:
1.如果這個View標籤屬性中只有id,沒有其他"@{表達式}"形式,則按照第2章提到的方式直接通過id查找;
2.如果這個View標籤屬性中有"@{表達式}"形式的值,則編譯器會自動給這個View加個android:tag="binding_{N}", 其中{N}按順序從0開始遞增,如android:tag="binding_0"。當執行ViewDataBinding#mapBindings去注入View時,會找tag為binding_開頭的View,隨後執行View注入;
另外,如果View標籤原來就有android:tag值,則編譯器會先保存原有值信息,寫入android:tag="binding_{N}"。當執行完view注入後,再把原來的值賦值給android:tag。注意如果原來的android:tag值為"binding_0",那麼在View注入時將會發生錯亂。
在完成View注入後,ActivityDetail3Binding會執行this.setRootTag(root),代碼如下:
這與ListView中的ViewHoloder實現方式相似,所以如果把DataBinding運用到ListView的ViewHolder中,就不需要多生成一個ViewHolder,直接使用這個ViewDataBinding類即可,例如ListAdapter實現:
8 總結DataBinding 庫非常小
目前Android Data Binding在運行類庫只有632個方法數,算上每個layout.xml自動生成的ViewDataBinding子類(demo中每個類不超過20個方法數),方法數總和也非常有限。
Data Binding方法數
DataBinding 運行時沒有多餘性能損耗
DataBinding所有的View注入、View賦值、Binding都是編譯器自動生成的代碼,這些重複的體力勞動本身就需要去做,只是交給了編譯器來完成,所以運行時沒有多餘的性能損耗。
DataBinding 可以減少錯誤率
既然View注入、View賦值、Binding都是編譯器自動完成的,只要使用正確,100%無低級錯誤保證,可以提高代碼質量,讓開發者心情愉悅。
DataBinding 對編譯時長的影響
還沒實際運用到生產環境,肯定有所延長,具體量級還未知。
官方Data-Binding-Guide
楊輝的個人博客-(譯)Data Binding 指南
LyndonChin/MasteringAndroidDataBinding
googlesource/data-binding