Android:Jetpack之視圖綁定——ViewBinding

1.Jetpack簡(jiǎn)介

手機(jī)廠商還沒(méi)卷完Android 12,Android 13就悄然聲息地來(lái)了,距離Google 2008年9月22日發(fā)布Android 1.0,已過(guò)去13個(gè)年頭。

歷經(jīng)13年的打磨和沉淀,Android體系與社區(qū)生態(tài)已非常成熟,開(kāi)發(fā)者從最初的框架少、沒(méi)規(guī)范、代碼都得自己寫,到輪子、框架滿天飛。得益于此,我們少做了很多臟活累活(基礎(chǔ)代碼),把更多的時(shí)間花在業(yè)務(wù)邏輯上,達(dá)成快速迭代的目的。

但琳瑯滿目的技術(shù)選型,也讓開(kāi)發(fā)者無(wú)從選擇,以致于做出的應(yīng)用良莠不齊,Android官方一直沒(méi)推出開(kāi)發(fā)標(biāo)準(zhǔn)。而一些技術(shù)社區(qū)出于更高效地進(jìn)行協(xié)同開(kāi)發(fā),逐漸引入了MVP、MVVM等應(yīng)用開(kāi)發(fā)架構(gòu)。使用這些架構(gòu)開(kāi)發(fā)出的應(yīng)用,從項(xiàng)目質(zhì)量、代碼可讀性與可維護(hù)性來(lái)說(shuō),都更加出色,所以這些框架和技術(shù)逐漸流行起來(lái)。

Google一直致力于Android生態(tài)環(huán)境的搭建,為了解決開(kāi)發(fā)碎片化,方便廣大開(kāi)發(fā)者,在2018年的 Google I/O大會(huì)上推出了全新的Android Jetpack應(yīng)用開(kāi)發(fā)架構(gòu)。它是一套庫(kù)、工具和指南的集合,稱作Jetpack開(kāi)發(fā)工具集可能更貼切。

Android Jetpack 向后兼容,是為現(xiàn)代設(shè)計(jì)實(shí)踐而設(shè)計(jì)的,如關(guān)注點(diǎn)分離、測(cè)試能力、松散耦合、觀察者模式、控制翻轉(zhuǎn)、Kotlin集成等生產(chǎn)力特性。旨在讓開(kāi)發(fā)者用更少的代碼,更易構(gòu)建出健壯、高質(zhì)量的應(yīng)用程序。

網(wǎng)上盛傳的一張將Jetpack組件分為四大類的老圖:

圖片來(lái)源:

簡(jiǎn)單介紹下~

Architecture → 架構(gòu)

幫助開(kāi)發(fā)者設(shè)計(jì)穩(wěn)健、可測(cè)試、易維護(hù)的應(yīng)用。

  • Data Binding數(shù)據(jù)綁定,可使用聲明式將布局中的界面組件綁定到應(yīng)用中的數(shù)據(jù)源;

  • Lifecycles生命周期感知,可感知和響應(yīng)Activity和Fragment的生命周期狀態(tài)的變化;

  • LiveData可觀察的數(shù)據(jù)持有者類,與常規(guī)Observable不同,它是具有生命周期感知的;

  • Navigation應(yīng)用內(nèi)導(dǎo)航,F(xiàn)ragment的管理框架,或者說(shuō)路由;

  • Paging列表分頁(yè),可以輕松實(shí)現(xiàn)分頁(yè)預(yù)加載以達(dá)到無(wú)限滑動(dòng)的效果;

  • Room輕量級(jí)ORM數(shù)據(jù)庫(kù),本質(zhì)上是一個(gè)SQLite抽象層,注解 + 編譯時(shí)自動(dòng)生成功能類;

  • ViewModel數(shù)據(jù)存儲(chǔ)組件,具備生命周期感知能力;

  • WorkManager托管延時(shí)任務(wù),即使APP被殺、或設(shè)備重啟,只要TaskRecord還存在最近訪問(wèn)列表中,都會(huì)執(zhí)行;

Foundation → 基礎(chǔ)

提供橫向功能,如:向后兼容、測(cè)試、安全、Kotlin語(yǔ)言支持;

  • AppCompat→ 幫助較低版本的Android系統(tǒng)進(jìn)行兼容;

  • Android KTX→ 基于Kotlin特性為Android、Jetpack提供一些簡(jiǎn)易易用的擴(kuò)展;

  • Multidex→ 為具有多個(gè)Dex文件應(yīng)用提供支持;

  • Test→ 用于單元和運(yùn)行時(shí)界面測(cè)試的 Android 測(cè)試框架;

  • Benchmark(性能檢測(cè))、Security(安全)等;

UI → 界面

  • Animation & Transition→ 內(nèi)置動(dòng)畫及自定義動(dòng)畫效果;

  • Emoji→ 即便用戶沒(méi)有更新Android系統(tǒng)也可以獲取最新的表情符號(hào);

  • Auto(車)、TV、WearOS;

  • Fragment→ 組件化界面的基本單位;

  • Layout→ 用XML中聲明UI元素或者在代碼中實(shí)例化UI元素;

  • Paletee→ 從調(diào)色板中提取出有用的信息;

Behavior → 行為

  • Download Manager→ 處理長(zhǎng)時(shí)間運(yùn)行的HTTP下載、超時(shí)重連的系統(tǒng)服務(wù);

  • Media & Playback→ 用于媒體播放和路由(包括 Google Cast)的向后兼容 API;

  • Permissions→ 用于檢查和請(qǐng)求應(yīng)用權(quán)限的兼容性API;

  • Notifications→ 提供向后兼容的通知API,支持Wear和Auto;

  • Sharing→ 提供適合應(yīng)用操作欄的共享操作;

  • Slices→ 一種UI模板,創(chuàng)建可在營(yíng)養(yǎng)外部顯示應(yīng)用數(shù)據(jù)的靈活界面元素;

雖然說(shuō),Android官網(wǎng)已經(jīng)找不到上面這個(gè)圖了,猜測(cè)官方旨在強(qiáng)化Architecture架構(gòu)組件,其他三個(gè)只是對(duì)已有內(nèi)容的收集整理。實(shí)際開(kāi)發(fā)中,也是這部分的組件用得多一些,Jetpack庫(kù)可單獨(dú)使用,也可以組合使用,開(kāi)發(fā)者可按需選擇。對(duì)此,官方還進(jìn)行了更細(xì)致的分類,具體可見(jiàn):

關(guān)于Jetpack的簡(jiǎn)介就到這里,在選型時(shí)弄清楚組件的存在緣由、責(zé)任邊界,就能有的放矢。本節(jié)開(kāi)始折騰,先帶來(lái)一個(gè)超簡(jiǎn)單的 → ViewBinding(視圖綁定)

2.從手寫findViewById 到ViewBinding

從早期對(duì)照XML手寫findViewById,到在線工具自動(dòng)生成:

到AS插件自動(dòng)生成:

再到View注入框架 ↓

后面Kotlin普及,帶來(lái)了擴(kuò)展創(chuàng)建kotlin-android-extensions(KAE)直接拿id當(dāng)控件用,原理:

類中定義一個(gè)存儲(chǔ)控件引用的HashMap,id為key,控件實(shí)例為value,當(dāng)用到控件時(shí),先查HashMap中該id對(duì)應(yīng)的實(shí)例是否緩存,是返回,否findViewById獲取實(shí)例存到HashMap中,同時(shí)把找到的實(shí)例返回。

粗暴的空間換時(shí)間,方便是挺方便的,但也存在下述問(wèn)題:

好景不長(zhǎng),Kotlin 1.4.20-M2中,JetBrains廢棄了KAE,轉(zhuǎn)而建議我們使用ViewBinding

3.ViewBinding基本用法

ViewBinding的作用:代替findViewById,還可以保證空安全和類型安全,支持Java。

:使用ViewBinding,AGP版本需 >= 3.6

接著介紹下基本用法,部分內(nèi)容搬運(yùn)自官方文檔:

① 啟用ViewBinding

需要啟用視圖綁定的Module,在其build.gradle添加下述配置:

android {       ...       viewBinding {           enabled = true       }   }   

不需要生成綁定類的布局XML文件,可在根節(jié)點(diǎn)中添加下述屬性:

<LinearLayout           ...           tools:viewBindingIgnore="true" >       ...   </LinearLayout>   

編譯后,AGP會(huì)為Module中包含的XML布局文件生成一個(gè)綁定類,類名規(guī)則:

XML文件名轉(zhuǎn)換為Pascal大小寫,并加上Binding,比如:result_profile.xml → ResultProfileBinding。

② 三個(gè)類綁定API

// View已存在   fun  <T>  bind(view : View) : T      // View未存在   fun  <T>  inflate(inflater : LayoutInflater) : T   fun  <T>  inflate(inflater : LayoutInflater, parent : ViewGroup?, attachToParent : Boolean) : T   

接下來(lái)演示一波各種場(chǎng)景下的ViewBinding用法,其實(shí)都是圍繞上述三個(gè)API進(jìn)行的~

③ Activity

class  MainActivity : AppCompatActivity() {       private lateinit var binding: ActivityMainBinding          override  fun  onCreate(savedInstanceState: Bundle?) {           super.onCreate(savedInstanceState)           // 1、實(shí)例化綁定實(shí)例           binding = ActivityMainBinding.inflate(layoutInflater)           // 2、獲得對(duì)根視圖的引用           val view = binding.root           // 3、讓根視圖稱為屏幕上的活動(dòng)視圖           setContentView(view)           // 4、引用視圖控件           binding.tvContent.text = "修改TextView文本"       }   }   

④ Fragment

class  ContentFragment: Fragment() {       private  var _binding: FragmentContentBinding? = null       private  val binding get() = _binding!!          override  fun  onCreateView(           inflater: LayoutInflater,           container: ViewGroup?,           savedInstanceState: Bundle?       ): View {           _binding = FragmentContentBinding.inflate(inflater, container, false)           return binding.root       }          override  fun  onViewCreated(view: View, savedInstanceState: Bundle?) {           super.onViewCreated(view, savedInstanceState)           binding.ivLogo.visibility = View.GONE       }          override  fun  onDestroyView() {           super.onDestroyView()           // Fragment的存活時(shí)間比View長(zhǎng),務(wù)必在此方法中清除對(duì)綁定類實(shí)例的所有引用           // 否則會(huì)引發(fā)內(nèi)存泄露           _binding = null       }   }   

如果布局已inflated,還可以采用另一種寫法(調(diào)bind):

class  TestFragment: Fragment(R.layout.fragment_content) {       private var _binding: FragmentContentBinding? = null          override  fun  onViewCreated(view: View, savedInstanceState: Bundle?) {           super.onViewCreated(view, savedInstanceState)           val binding = FragmentContentBinding.bind(view)           _binding = binding           binding.ivLogo.visibility = View.VISIBLE       }          override  fun  onDestroyView() {           super.onDestroyView()           // 同樣需要置空           _binding = null       }   }   

⑤ Dialog

如果是繼承DialogFragment寫法同F(xiàn)ragment,如果是繼承Dialog寫法示例如下(PopupWindow類似)~

class  TestDialog(context: Context) : Dialog(context) {       override  fun  onCreate(savedInstanceState: Bundle?) {           super.onCreate(savedInstanceState)           val binding = DialogTestBinding.inflate(layoutInflater)           setContentView(binding.root)           binding.tvTitle.text = "對(duì)話框標(biāo)題"       }   }   

⑥ RecyclerView

class  TestAdapter(list: List<String>) : RecyclerView.Adapter<TestAdapter.ViewHolder>() {       private  var mList: List<String> = list          override  fun  onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {           // 需在此初始化以獲得父類容器           val binding = ItemTestBinding.inflate(LayoutInflater.from(parent.context), parent, false)           return ViewHolder(binding)       }          override  fun  onBindViewHolder(holder: ViewHolder, position: Int) {           holder.tvItem.text = "Adapter"       }          override  fun  getItemCount() = mList.size          // 傳遞Binding對(duì)象       class  ViewHolder(binding: ItemTestBinding) : RecyclerView.ViewHolder(binding.root) {           var tvItem: TextView = binding.tvItem       }   }   

⑦ 自定義ViewGroup

ViewGroup子類才能使用視圖綁定,View子類不可使用,示例如下:

class  TestLayout: LinearLayout {       constructor(context: Context): super(context)       constructor(context: Context, attrs: AttributeSet): super(context, attrs) {           val inflater = LayoutInflater.from(this.context)           val binding = ItemLayoutBinding.inflate(inflater, this, true)           binding.tvLayout.text = "自定義ViewGroup"       }          override fun onDraw(canvas: Canvas?) {           super.onDraw(canvas)       }   }   

⑧ include

根據(jù)include的布局xml是否帶<merge>標(biāo)簽,分為兩種,先是不帶的情況: include的xml文件名為sub_include_test.xml,id為include_layout:

然后是帶的情況,布局文件改下:

使用部分的代碼不變,運(yùn)行奔潰報(bào)錯(cuò)信息如下:

原因是merge并不會(huì)加載到布局里,解法:把include標(biāo)簽的id去掉,然后bind傳入父布局~

⑨ ViewStub

基礎(chǔ)用法很簡(jiǎn)單,也很好上手,但存在下述問(wèn)題:

需重復(fù)編寫:創(chuàng)建和回收ViewBinding實(shí)例的樣板代碼,特別是Fragment,還要手動(dòng)置空。

所以有必要封裝優(yōu)化一波~

4.封裝優(yōu)化思路

① 泛型 + 父類實(shí)現(xiàn)模板代碼

最容易想到的常規(guī)寫法,配合泛型,把模板代碼都在父類中寫好,非常簡(jiǎn)單:

abstract  class  BaseFragment<T : ViewBinding>(layoutId: Int) : Fragment(layoutId) {       private  var _binding: T? = null       val binding get() = _binding!!          override  fun  onViewCreated(view: View, savedInstanceState: Bundle?) {           super.onViewCreated(view, savedInstanceState)           _binding = initBinding(view)           init()       }          abstract  fun  initBinding(view: View): T          abstract  fun  init()          override  fun  onDestroyView() {           _binding = null           super.onDestroyView()       }   }      // 子類實(shí)現(xiàn)   class  TestFragment : BaseFragment<FragmentContentBinding>(R.layout.fragment_content) {       override  fun  initBinding(view: View) = FragmentContentBinding.bind(view)          override  fun  init() {           binding.ivLogo.visibility = View.VISIBLE       }      }   

② Kotlin委托 + lifecycle組件

有些朋友可能覺(jué)得寫在父類中侵入性太強(qiáng),接著試下用其他方式進(jìn)行封裝,先看原始Activity:

要把圈住的代碼干掉,先是泛型傳遞問(wèn)題,泛型在進(jìn)JVM前會(huì)被擦除,可在運(yùn)行時(shí)通過(guò)反射獲得,還可以通過(guò)實(shí)例化類類型代替類引用,如:

fun <T: Activity> FragmentActivity.startActivity(context: Context, clazz: Class<T>) {       startActivity(Intent(context, clazz))   }      // 調(diào)用處   startActivity(context, MainActivity::class.java)   

而在Kotlin中還可以用inline定義一個(gè)內(nèi)聯(lián)函數(shù)(編譯時(shí)自動(dòng)替換到調(diào)用位置),配合reified具體化(類型不擦除),得到泛型類型的Class,如:

inline  fun <reified T : Activity> Activity.startActivity(context: Context) {       startActivity(Intent(context, T::class.java))   }      // 調(diào)用   startActivity<MainActivity>(context)   

可以,配合反射invoke()調(diào)用,隨手寫上一個(gè)擴(kuò)展方法:

調(diào)用下:

看似十拿九穩(wěn),結(jié)果一跑就崩:

不過(guò)也在意料之內(nèi),Activity還沒(méi)onCreate()就初始化了,不空才怪,可以利用標(biāo)準(zhǔn)委托-lazy延遲初始化,修改后的代碼:

調(diào)用下:

運(yùn)行通過(guò),你還可以把還可以把setContentView()也塞到擴(kuò)展中:

配合lifecycle組件,順手把Fragment的也寫出來(lái):

調(diào)用下:

對(duì)了,如果還不想使用反射,可以利用Kotlin高階函數(shù),示例如下:

調(diào)用下:

嘖嘖嘖,你還可以不用lazy,自己重寫ReadOnlyProperty,這里只是試試水封裝,并沒(méi)用到生產(chǎn)上,更完善的封裝方案可自行參考下述開(kāi)源庫(kù):

5.原理

AGP會(huì)為模塊中每個(gè)XML生成一個(gè)綁定類,該類的實(shí)例會(huì)直接引用布局中聲明了資源id的View

① 自動(dòng)生成的綁定類

打開(kāi):module模塊名/build/generated/intermediates/javac/渠道/包名/databinding

可以看到 (基于AGP 7.1.1,不同AGP版本可能不一樣):

自動(dòng)生成的class文件,隨手打開(kāi)一個(gè):

所以本質(zhì)上還是findViewById,只是自動(dòng)生成了控件實(shí)例,并一一對(duì)應(yīng),接著簡(jiǎn)單了解下大概的生成流程。

② 生成Java類

執(zhí)行g(shù)radlew assembleDebug,在Task構(gòu)建列表沒(méi)找到ViewBinding,卻找到了DataBinding:

打開(kāi)AGP源碼,全局搜dataBindingMergeGenClassesDataBindingMergeBaseClassLogTask.kt

跟到:TaskManager.ktcreateDataBindingTasksIfNecessary

2333,跟DataBinding混一起了,所以ViewBinding其實(shí)只是DataBinding功能的一小部分~

看回:DataBindingMergeBaseClassLogTask,增量和全量執(zhí)行動(dòng)作:

跟下:DataBindingMergeBaseClassLogDelegate

跟下:DataBindingMergeBaseClassLogRunnable

判斷文件是否新建、修改、或移除,跟下是哪個(gè)文件:

全局搜下這個(gè)結(jié)尾的文件,在下述目錄找到了它:

不難看出是:XML名稱和ViewBinding類的映射,往下看DataBindingMergeDependencyArtifactsTask,BR相關(guān)的,目前不知道是干嘛的。再往下走:DataBindingGenBaseClassesTask → CreationAction

跟下:DataBindingGenBaseClassesTask → @TaskAction

先看buildInputArgs(),構(gòu)建輸入?yún)?shù),同樣對(duì)增量和全量編譯進(jìn)行了不同的處理,然后返回配置實(shí)例

接著看CodeGenerator類,見(jiàn)名知意,代碼生成器

這里直接索引不到BaseDataBinder,需要另外依賴:databinding-compiler-common

implementation 'androidx.databinding:databinding-compiler-common:7.1.0'   

async后就可以了,打開(kāi)

可以看到它依賴了47個(gè)運(yùn)行時(shí)庫(kù)~

跟下:BaseDataBinder → generateAll()

跟下:ViewBinderGenerateJava.kt → toJavaFile() → JavaFileGenerator

Java文件就是從這里構(gòu)造出來(lái)的,具體構(gòu)造過(guò)程,感興趣的可以自己翻閱下此文件。

另外,如果你想了解布局采集和寫Layout部分的邏輯,可以參考

筆者卷不動(dòng)了...

6.一些補(bǔ)充

① 與DataBinding的區(qū)別

可以把ViewBinding看做DataBinding功能的子集,它有的功能DataBinding都有,不需要數(shù)據(jù)綁定單純想替代findViewById可以用ViewBinding。

② 不用build就能自動(dòng)生成Java類

筆者猜測(cè):AS起了一個(gè)進(jìn)程Filesystem events processor用于監(jiān)聽(tīng)文件變化,有文件變動(dòng)時(shí)回調(diào)執(zhí)行ViewBinding相關(guān)的Task。

③ KAE庫(kù)過(guò)時(shí),遷移Parcelable

Module層次的build.gradle添加kotlin-parcelize插件。

以上就是本節(jié)的全部?jī)?nèi)容,有疑問(wèn)或補(bǔ)充歡迎評(píng)論區(qū)指出,謝謝~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容