Android:Jetpack之視圖綁定——ViewBinding

1.Jetpack簡介

手機廠商還沒卷完Android 12婿禽,Android 13就悄然聲息地來了赏僧,距離Google 2008年9月22日發(fā)布Android 1.0,已過去13個年頭谈宛。

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

但琳瑯滿目的技術選型侄柔,也讓開發(fā)者無從選擇共啃,以致于做出的應用良莠不齊,Android官方一直沒推出開發(fā)標準暂题。而一些技術社區(qū)出于更高效地進行協(xié)同開發(fā)移剪,逐漸引入了MVP、MVVM等應用開發(fā)架構薪者。使用這些架構開發(fā)出的應用纵苛,從項目質量、代碼可讀性與可維護性來說言津,都更加出色攻人,所以這些框架和技術逐漸流行起來。

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

Android Jetpack 向后兼容渣窜,是為現(xiàn)代設計實踐而設計的,如關注點分離宪躯、測試能力乔宿、松散耦合、觀察者模式访雪、控制翻轉详瑞、Kotlin集成等生產(chǎn)力特性。旨在讓開發(fā)者用更少的代碼臣缀,更易構建出健壯坝橡、高質量的應用程序。

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

圖片來源:

簡單介紹下~

Architecture → 架構

幫助開發(fā)者設計穩(wěn)健精置、可測試计寇、易維護的應用。

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

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

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

  • Navigation應用內導航棋电,F(xiàn)ragment的管理框架茎截,或者說路由;

  • Paging列表分頁赶盔,可以輕松實現(xiàn)分頁預加載以達到無限滑動的效果企锌;

  • Room輕量級ORM數(shù)據(jù)庫,本質上是一個SQLite抽象層招刨,注解 + 編譯時自動生成功能類霎俩;

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

  • WorkManager托管延時任務打却,即使APP被殺、或設備重啟谎倔,只要TaskRecord還存在最近訪問列表中柳击,都會執(zhí)行;

Foundation → 基礎

提供橫向功能片习,如:向后兼容捌肴、測試、安全藕咏、Kotlin語言支持状知;

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

  • Android KTX→ 基于Kotlin特性為Android孽查、Jetpack提供一些簡易易用的擴展饥悴;

  • Multidex→ 為具有多個Dex文件應用提供支持;

  • Test→ 用于單元和運行時界面測試的 Android 測試框架盲再;

  • Benchmark(性能檢測)西设、Security(安全)等;

UI → 界面

  • Animation & Transition→ 內置動畫及自定義動畫效果答朋;

  • Emoji→ 即便用戶沒有更新Android系統(tǒng)也可以獲取最新的表情符號贷揽;

  • Auto(車)、TV梦碗、WearOS禽绪;

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

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

  • Paletee→ 從調色板中提取出有用的信息丐一;

Behavior → 行為

  • Download Manager→ 處理長時間運行的HTTP下載藻糖、超時重連的系統(tǒng)服務淹冰;

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

  • Permissions→ 用于檢查和請求應用權限的兼容性API;

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

  • Sharing→ 提供適合應用操作欄的共享操作;

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

雖然說,Android官網(wǎng)已經(jīng)找不到上面這個圖了正罢,猜測官方旨在強化Architecture架構組件阵漏,其他三個只是對已有內容的收集整理。實際開發(fā)中翻具,也是這部分的組件用得多一些履怯,Jetpack庫可單獨使用,也可以組合使用裆泳,開發(fā)者可按需選擇叹洲。對此,官方還進行了更細致的分類工禾,具體可見:

關于Jetpack的簡介就到這里运提,在選型時弄清楚組件的存在緣由、責任邊界闻葵,就能有的放矢民泵。本節(jié)開始折騰,先帶來一個超簡單的 → ViewBinding(視圖綁定)槽畔。

2.從手寫findViewById 到ViewBinding

從早期對照XML手寫findViewById栈妆,到在線工具自動生成:

到AS插件自動生成:

再到View注入框架 ↓

后面Kotlin普及,帶來了擴展創(chuàng)建kotlin-android-extensions(KAE)竟痰,直接拿id當控件用签钩,原理:

類中定義一個存儲控件引用的HashMap,id為key坏快,控件實例為value铅檩,當用到控件時,先查HashMap中該id對應的實例是否緩存莽鸿,是返回昧旨,否findViewById獲取實例存到HashMap中拾给,同時把找到的實例返回。

粗暴的空間換時間兔沃,方便是挺方便的蒋得,但也存在下述問題:

好景不長,Kotlin 1.4.20-M2中乒疏,JetBrains廢棄了KAE额衙,轉而建議我們使用ViewBinding

3.ViewBinding基本用法

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

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

接著介紹下基本用法伟件,部分內容搬運自官方文檔:

① 啟用ViewBinding

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

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

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

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

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

XML文件名轉換為Pascal大小寫煞肾,并加上Binding咧织,比如:result_profile.xml → ResultProfileBinding。

② 三個類綁定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   

接下來演示一波各種場景下的ViewBinding用法扯旷,其實都是圍繞上述三個API進行的~

③ Activity

class  MainActivity : AppCompatActivity() {       private lateinit var binding: ActivityMainBinding          override  fun  onCreate(savedInstanceState: Bundle?) {           super.onCreate(savedInstanceState)           // 1拯爽、實例化綁定實例           binding = ActivityMainBinding.inflate(layoutInflater)           // 2、獲得對根視圖的引用           val view = binding.root           // 3钧忽、讓根視圖稱為屏幕上的活動視圖           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的存活時間比View長,務必在此方法中清除對綁定類實例的所有引用           // 否則會引發(fā)內存泄露           _binding = null       }   }   

如果布局已inflated耸黑,還可以采用另一種寫法(調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 = "對話框標題"       }   }   

⑥ 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對象       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>標簽为迈,分為兩種,先是不帶的情況: include的xml文件名為sub_include_test.xml缺菌,id為include_layout:

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

使用部分的代碼不變,運行奔潰報錯信息如下:

原因是merge并不會加載到布局里伴郁,解法:把include標簽的id去掉耿战,然后bind傳入父布局~

⑨ ViewStub

基礎用法很簡單,也很好上手焊傅,但存在下述問題:

需重復編寫:創(chuàng)建和回收ViewBinding實例的樣板代碼剂陡,特別是Fragment狈涮,還要手動置空。

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

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

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

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

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()       }   }      // 子類實現(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組件

有些朋友可能覺得寫在父類中侵入性太強松却,接著試下用其他方式進行封裝,先看原始Activity:

要把圈住的代碼干掉捏题,先是泛型傳遞問題玻褪,泛型在進JVM前會被擦除,可在運行時通過反射獲得公荧,還可以通過實例化類類型代替類引用,如:

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

而在Kotlin中還可以用inline定義一個內聯(lián)函數(shù)(編譯時自動替換到調用位置)同规,配合reified具體化(類型不擦除)循狰,得到泛型類型的Class,如:

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

可以券勺,配合反射invoke()調用绪钥,隨手寫上一個擴展方法:

調用下:

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

不過也在意料之內关炼,Activity還沒onCreate()就初始化了程腹,不空才怪,可以利用標準委托-lazy延遲初始化儒拂,修改后的代碼:

調用下:

運行通過寸潦,你還可以把還可以把setContentView()也塞到擴展中:

配合lifecycle組件,順手把Fragment的也寫出來:

調用下:

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

調用下:

嘖嘖嘖蒜哀,你還可以不用lazy斩箫,自己重寫ReadOnlyProperty,這里只是試試水封裝撵儿,并沒用到生產(chǎn)上乘客,更完善的封裝方案可自行參考下述開源庫:

5.原理

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

① 自動生成的綁定類

打開:module模塊名/build/generated/intermediates/javac/渠道/包名/databinding

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

自動生成的class文件易核,隨手打開一個:

所以本質上還是findViewById,只是自動生成了控件實例房匆,并一一對應耸成,接著簡單了解下大概的生成流程报亩。

② 生成Java類

執(zhí)行gradlew assembleDebug,在Task構建列表沒找到ViewBinding井氢,卻找到了DataBinding:

打開AGP源碼弦追,全局搜dataBindingMergeGenClassesDataBindingMergeBaseClassLogTask.kt

跟到:TaskManager.ktcreateDataBindingTasksIfNecessary

2333,跟DataBinding混一起了花竞,所以ViewBinding其實只是DataBinding功能的一小部分~

看回:DataBindingMergeBaseClassLogTask劲件,增量和全量執(zhí)行動作:

跟下:DataBindingMergeBaseClassLogDelegate

跟下:DataBindingMergeBaseClassLogRunnable

判斷文件是否新建、修改约急、或移除零远,跟下是哪個文件:

全局搜下這個結尾的文件,在下述目錄找到了它:

不難看出是:XML名稱和ViewBinding類的映射厌蔽,往下看DataBindingMergeDependencyArtifactsTask牵辣,BR相關的,目前不知道是干嘛的奴饮。再往下走:DataBindingGenBaseClassesTask → CreationAction

跟下:DataBindingGenBaseClassesTask → @TaskAction

先看buildInputArgs()纬向,構建輸入?yún)?shù),同樣對增量和全量編譯進行了不同的處理戴卜,然后返回配置實例

接著看CodeGenerator類逾条,見名知意,代碼生成器

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

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

async后就可以了师脂,打開

可以看到它依賴了47個運行時庫~

跟下:BaseDataBinder → generateAll()

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

Java文件就是從這里構造出來的榨了,具體構造過程织盼,感興趣的可以自己翻閱下此文件。

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

筆者卷不動了...

6.一些補充

① 與DataBinding的區(qū)別

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

② 不用build就能自動生成Java類

筆者猜測:AS起了一個進程Filesystem events processor用于監(jiān)聽文件變化,有文件變動時回調執(zhí)行ViewBinding相關的Task浮毯。

③ KAE庫過時完疫,遷移Parcelable

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

以上就是本節(jié)的全部內容债蓝,有疑問或補充歡迎評論區(qū)指出壳鹤,謝謝~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市饰迹,隨后出現(xiàn)的幾起案子芳誓,更是在濱河造成了極大的恐慌余舶,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锹淌,死亡現(xiàn)場離奇詭異匿值,居然都是意外死亡,警方通過查閱死者的電腦和手機赂摆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門挟憔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人烟号,你說我怎么就攤上這事绊谭。” “怎么了汪拥?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵达传,是天一觀的道長。 經(jīng)常有香客問我喷楣,道長趟大,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任铣焊,我火速辦了婚禮,結果婚禮上罕伯,老公的妹妹穿的比我還像新娘曲伊。我一直安慰自己,他們只是感情好追他,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布坟募。 她就那樣靜靜地躺著,像睡著了一般邑狸。 火紅的嫁衣襯著肌膚如雪懈糯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天单雾,我揣著相機與錄音赚哗,去河邊找鬼。 笑死硅堆,一個胖子當著我的面吹牛屿储,可吹牛的內容都是我干的。 我是一名探鬼主播渐逃,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼够掠,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了茄菊?” 一聲冷哼從身側響起疯潭,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤赊堪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后竖哩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哭廉,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年期丰,在試婚紗的時候發(fā)現(xiàn)自己被綠了群叶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡钝荡,死狀恐怖街立,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情埠通,我是刑警寧澤赎离,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站端辱,受9級特大地震影響梁剔,放射性物質發(fā)生泄漏。R本人自食惡果不足惜舞蔽,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一荣病、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧渗柿,春花似錦个盆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至陨溅,卻和暖如春终惑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背门扇。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工雹有, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人悯嗓。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓件舵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親脯厨。 傳聞我的和親對象是個殘疾皇子铅祸,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內容