1. 概述
KAE(kotlin-android-extensions)插件在Kotlin 1.4.20版本開始被廢棄,視圖綁定(ViewBinding)是其遷移方案译断。
更多內容可參照谷歌開發(fā)者公眾號在2020-12-04的如下推送:
https://mp.weixin.qq.com/s/pa1YOFA1snTMYhrjnWqIgg
2. 視圖綁定是什么
ViewBiding授翻,視圖綁定,就是將日常開發(fā)工作中的通過xml來inflate
成View對象和對視圖元素進行findViewById
進行對象獲取的過程孙咪,封裝到了具體的視圖綁定類當中堪唐。同時,這個類的生成翎蹈,交于Android Studio中完成淮菠,無需開發(fā)者手動參與。
更具體點荤堪,就是Android Studio會根據(jù)布局xml生成一個對應命名的Binding類合陵,類內成員屬性時帶有xml根元素和每個帶有id的視圖元素(除非顯式設置tools:viewBindingIgnore="true"
),并其中帶有對應xml的靜態(tài)inflate
和bind
方法用以構造視圖綁定對象澄阳。
一言以概之拥知,視圖綁定類就是對xml視圖元素的包裹類!
所以碎赢,只要開發(fā)者會寫視圖的inflate
和findViewById
方法举庶,那么便無學習成本地會用視圖綁定!
3. 視圖綁定的理解視角
概述如下:
視圖綁定類對象是對xml視圖元素的包裹類對象揩抡;
視圖綁定的靜態(tài)
bind
方法就是通過對根視圖逐一findViewById
獲取元素對象引用,進而構造視圖綁定類對象镀琉;視圖綁定的靜態(tài)
inflate
方法是從xml布局創(chuàng)建視圖對象方法并同時調用bind
函數(shù)獲得視圖綁定對象的封裝峦嗤;
所以,視圖綁定并沒有帶來新的學習內容屋摔,只是新壺(視圖綁定類)裝舊酒(inflate烁设、findViewById),而且這個”裝酒“的過程交給Android Studio去完成,進而減少了開發(fā)者的重復性工作装黑。
所以副瀑,視圖綁定可以說是幾乎無學習成本的!
如果對概述的內容有所疑惑恋谭,請繼續(xù)往下看:
假定有以下Java代碼:
View view = LayoutInflater.from(context).inflate(R.layout.view_test, parent, false);
TextView nameTv = view.findViewById(R.id.name_tv);
TextView mailTv = view.findViewById(R.id.mail_tv);
這類代碼應該是不采用ButterKnife糠睡、KAE、ViewBinding等任何方案時疚颊,非常常見的一種代碼方式狈孔。
如果用視圖綁定,這里就縮減成如下方式:
ViewTestBinding binding = ViewTestBinding.inflate(LayoutInflater.from(context), parent, false);
拿到視圖綁定引用后材义,就可以非常方便地任意訪問xml里的對應元素了:
binding.getRoot() // xml根視圖
binding.nameTv; // xml中的name_tv
binding.mailTv; // xml中的mail_tv
這樣均抽,即使xml里有再多的視圖元素要訪問,再也不用手動一個個地去寫findViewById
了其掂,而是轉而通過視圖綁定對象去統(tǒng)一訪問油挥。
所以,視圖綁定類就是對xml視圖元素的包裹類款熬,僅此而已深寥。
而視圖綁定的類,由Android Studio自行根據(jù)xml實際情況生成并修改华烟,并不用開發(fā)者手動維護翩迈。
那么,視圖綁定是何時給xml里的視圖元素對應找到視圖對象引用的呢盔夜?答案就在視圖綁定類的靜態(tài)方法里负饲。
每個視圖綁定類都必定包含下面三個靜態(tài)方法:
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater)
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent)
@NonNull
public static ViewTestBinding bind(@NonNull View rootView)
這也是初看視圖綁定時比較困惑的地方,因為各類文檔中用了不同的靜態(tài)方法去獲取視圖對象實例喂链。
但是返十,這三者其實是統(tǒng)一的,一個參數(shù)的inflate
方法調用了三個參數(shù)的inflate方法椭微,三個參數(shù)的inflate
方法其實調用的是bind
方法洞坑。
也就是說,無論用的是哪個靜態(tài)方法去獲取視圖對象對象蝇率,其實最終都是用的都是bind
迟杂!
而對于inflate
方法,其實也不過是LayoutInflater#inflate(int, ViewGroup, boolean)
的簡單封裝:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
目的本慕,是為了封裝對應的xml布局到inflate
方法內部(視圖綁定類命名本身已經與對應的xml布局文件所一一對應起來)排拷。
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ViewTestBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.view_test, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
而bind
方法里,則是最后根據(jù)對于的根視圖來對所有視圖元素進行findViewById
的過程(枯燥乏味的過程锅尘,這里不貼代碼了监氢,況且每個xml里會有不同的id和數(shù)量);
想明白了上面的過程,其實視圖綁定還可以這么用
View view = LayoutInflater.from(context).inflate(R.layout.view_test, parent, false);
ViewTestBinding binding = ViewTestBinding.bind(view);
這樣出來的binding引用和直接用inflate
獲取的引用其實是一樣的效果浪腐,只不過這種寫法纵揍,有兩點重復的內容了:
- 視圖綁定類本身即是與layout的文件名匹配生成的,兩者出現(xiàn)其一就足夠了议街,這里兩者都出現(xiàn)了泽谨;
- 這里的view引用和通過
binding.getRoot()
獲得的引用會是同一個(但類型不同),同一個引用可以通過兩種方式訪問了傍睹;
這兩點影響較小隔盛,不過各處文檔中都沒有出現(xiàn)這種寫法,個人猜測拾稳,可能是考慮到啰嗦與重復了吧吮炕,畢竟一個函數(shù)調用能做完的事情,為什么要分兩步访得?
4. 視圖綁定在Activity/Fragment中的用法
這節(jié)來結合官方開發(fā)文檔中視圖綁定的用法實例來看下龙亲,
注,下面示例代碼來自:https://developer.android.google.cn/topic/libraries/view-binding#kotlin
4.1 在Activity中使用視圖綁定
官方樣例代碼:
private lateinit var binding: ResultProfileBinding
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
看到這個代碼悍抑,怎么還能說視圖綁定是無學習成本的呢鳄炉?不是還要看視圖綁定的inflate
方法怎么用嗎?
但是呢搜骡,有沒覺得拂盯,這個inflate
方法很眼熟?其實吧记靡,這個寫法谈竿,是精簡寫法了,不信摸吠?一步步來拆解一下:
沒有視圖綁定之前空凸,其實可以這樣去給Activity去setContentView
:
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
val view = layoutInflater.inflate(R.layout.result_profile, null, false)
setContentView(view)
}
只是,普遍寫法是:setContentView(R.layout.result_profile)
普遍寫法不代表只有這種方式寸痢,而視圖綁定方式呀洲,只是將上面layoutInflater.inflate
的方式進行了一層封裝并同時構造了視圖綁定對象。
其實吧啼止,如果不想改變原來的setContentView(R.layout.xxx)
的方式道逗,又想獲取視圖綁定實例,也是有辦法的献烦,如下:
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.result_profile)
binding = ActivityMainBinding.bind(findViewById<ViewGroup>(android.R.id.content).getChildAt(0))
}
效果和官方示例其實是一樣的憔辫,只是,麻煩和啰嗦了一些仿荆。這里僅為說明視圖綁定的情況,不代表個人建議如上寫法。
4.2 在Fragment中使用視圖綁定
官方樣例代碼:
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
可能有一部分開發(fā)者會疑惑拢操,為什么這里要對視圖綁定屬性進行置空呢锦亦?上面Activity使用過程中卻不需要?
概述性的描述為:不置空會有可能的內存泄漏風險令境。
但是杠园,這個并不是視圖綁定所帶來的風險,更直接地說舔庶,這個鍋抛蚁,視圖綁定不接。
為講清楚這個惕橙,先從Java代碼中說起瞧甩,在沒有使用視圖綁定之前,有下面代碼:
private View mRootView;
@Override
public View onCreateView (LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
mRootView = inflater.inflate(R.layout.result_profile, container, false);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
mRootView = null;
}
如果在onDestroyView
沒有對mRootView
進行置空弥鹦,那么原理上同樣地存在內存泄漏風險肚逸。
事實上,上述mRootView
的寫法即使沒有在onDestroyView中進行相應的置空彬坏,大多情況下也不會內存泄漏(LeakCanary等內存泄漏檢測結果)朦促,這又是為什么呢?
這是因為大多使用Fragment的過程中栓始,很少見有FragmentTransaction#attach
和FragmentTransaction#detach
的使用务冕,又或者不常用FragmentPagerAdapter
(里面對于Fragment的切換是用attach/detach
方法),所以Fragment與View的生命周期幾乎等同進而不會有內存泄漏問題幻赚。
回過來禀忆,視圖綁定類就是對xml視圖元素的包裹類,對于視圖綁定在Fragment中的內存泄漏風險坯屿,并不是視圖綁定的鍋油湖,而是對Fragment和View之間的設計以及內存泄漏原理理解不足夠產生的“偏見”,如果能處理好Fragment與View之間的關系领跛,自然也能在Fragment中用好視圖綁定乏德。
題外話,其實在業(yè)務Fragment中保存mRootView
其實是多余的吠昭,在onCreateView
執(zhí)行后并且在onDestroyView
執(zhí)行后不久的這段生命周期內喊括,通過Fragment#getView
方法拿出來的即是onCreateView
中的返回值,有興趣的可以從源碼上查看Fragment中的生命周期與視圖View之間的詳細設計矢棚。
5. 關于視圖綁定需要說明的
5.1 視圖綁定跟模塊相關
模塊(module)下的build.gradle
的中郑什,增加:
android {
...
buildFeatures {
viewBinding = true
}
...
}
只有增加了該內容的module才會開啟視圖綁定功能,為module下的layout中的xml生成對應的視圖綁定類蒲肋,沒有該部分內容的module則不會
由于視圖綁定類是代碼文件蘑拯,可以引用別的module中的視圖綁定類來用钝满。
5.2 關于tools:viewBindingIgnore="true"
放入xml根布局,則不生成對應的視圖綁定類申窘。
放入視圖id元素聲明統(tǒng)計弯蚜,則不生成該id對應的視圖屬性。
屬于優(yōu)化生成的視圖綁定類代碼內容的優(yōu)化項剃法。
5.3 視圖綁定對于xml中的include處理
視圖綁定類的是以xml布局文件作為維度的薪伏,所以僅會對當前xml中的視圖id元素進行屬性生成其馏,而xml中include另一個xml的內容相關不會進行相應屬性生成。
如果對第4節(jié)的內容理解清楚,那其實include的內容就是通過視圖綁定的靜態(tài)bind
方法就可以生成相應的視圖綁定對象來使用槐沼。
當然洒沦,在include標簽里新聲明id且被include的xml里沒有使用merge標簽時也可以一步到位瓷们,但是吧界牡,如果被include的xml根布局也有id,情況就有點多了俩块。
這里不繼續(xù)討論黎休,因為不同人不同場景可能對這部分有不同的選擇,更多場景應該是findViewById和視圖id之間的關聯(lián)和理解玉凯,而非視圖綁定本身需要關注的內容势腮。
6. 關于對視圖綁定的進一步封裝
封裝是一種避免犯錯規(guī)范使用的一種好方案,但是視圖綁定從本身設計上和使用上就足夠簡單漫仆,為了更簡潔和更安全捎拯,再進行一層一層的封裝,一定程度加大了學習理解和后續(xù)維護拓展的成本盲厌。
與其理解使用對于視圖綁定與各項其他內容的封裝以及維護相應的邏輯署照,不如把重點放在視圖綁定和視圖框架本身的理解和使用上。
目前吗浩,網(wǎng)上對于視圖綁定有各種八仙過海式的封裝使用建芙,如反射、基類封裝懂扼、Kotlin的具化泛型禁荸、Kotlin屬性委托方式等等,封裝的角度和考慮往往相當深入阀湿,其中個人看到的屬性委托方式封裝視圖綁定的技術內容更是將Kotlin的屬性委托赶熟、Jetpack中的LifeCycle組件、內存泄漏問題研究得妥妥帖帖陷嘴。
如果從學習和深入的角度去看待各種封裝思想和用處映砖,這是非常好的一件事,但是反過來灾挨,視圖綁定本身使用上就不復雜不難用邑退,看到各路封裝容易讓不明所以的人容易對視圖綁定有個不容易用的錯覺竹宋。
即使不進行任何封裝,本身視圖綁定的使用就是一行或者幾行代碼的事情地技,至于對于內存泄漏逝撬,本身的解決方案也僅是在恰當?shù)奈恢弥每找膊⒉粡碗s,這里更多需要注意的是對于Fragment/View/內存泄漏的理解乓土,治標不如治本。
7. 為什么要從KAE換用視圖綁定
無法否認的一點是溯警,視圖綁定在使用上趣苏,并沒有KAE中直接通過id訪問這么方便,那么為什么要從KAE換用視圖綁定方案呢梯轻?
個人角度的一些想法:
- KAE僅支持Kotlin代碼食磕,視圖支持Kotlin和Java;
另一種觀點:現(xiàn)在主推Kotlin開發(fā)代碼了喳挑,Java是否能用影響不大彬伦。
-
KAE在每個使用id訪問視圖的地方使用
HashMap
并開發(fā)代碼中嵌入了相應代碼,造成了運行時內存額外占用和更大的代碼打包體積伊诵;
視圖綁定也會產生相應的代碼生成单绑,不過多處使用的視圖綁定都是同一個所以代碼打包增加量是個常數(shù),包裝類也有內存占用成本曹宴,但是成本也小于HashMap
的使用搂橙。
另一種觀點:HashMap
這一點點的內存占用成本和代碼打包體積在整體工程項目下往往可以忽略不計。
- KAE并不是空安全的笛坦,而視圖綁定對空安全的支持更好区转;
因為一個視圖id即使不在當前的視圖布局內,KAE使用上在代碼開發(fā)階段也可以通過id去訪問版扩,只有在運行時才會出現(xiàn)異常废离,而視圖綁定則因為通過視圖綁定對象限定了視圖布局的范圍更安全;再者在橫豎屏的xml有不同元素時礁芦,視圖綁定會標注視圖是否可空蜻韭,而KAE有空安全風險。
杠:也不是什么問題宴偿,運行到了時候出現(xiàn)異常再檢查修改湘捎,分分鐘的事。
答:萬一這個視圖訪問的邏輯業(yè)務層次很深窄刘,不容易觸發(fā)該部分業(yè)務邏輯的話窥妇,存在一定風險。
杠:那就是代碼邏輯性和測試的問題了娩践,規(guī)范嚴謹?shù)拈_發(fā)者不會犯這樣的錯誤的活翩。
答:烹骨。。材泄。沮焕。。拉宗。
最后峦树,也是最重要的一點,那就是KAE已經被官方廢旦事!棄魁巩!了!姐浮,替代方案是視圖綁定谷遂。
8. 總結
其實視圖綁定處于一個挺尷尬的時期,前有KAE更方便的使用卖鲤,后有Jetpack Compose這種聲明式UI架構正在路上肾扰。KAE被廢棄了,開發(fā)者應避免使用被廢棄的內容蛋逾;Jetpack Compose是聲明式UI集晚,對于現(xiàn)存的命令式UI開發(fā)框架是個較大的沖擊,加上官方在逐步優(yōu)化Compose性能换怖、新框架的學習遷移成本等因素甩恼,這個應該仍需一定的時間和過程。
好在沉颂,視圖綁定內容本身幾乎沒有學習成本条摸,使用上也并不麻煩,所以铸屉,在現(xiàn)有的命令式UI開發(fā)框架下钉蒲,仍可一戰(zhàn)!