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源碼,全局搜dataBindingMergeGenClasses→DataBindingMergeBaseClassLogTask.kt
跟到:TaskManager.kt→createDataBindingTasksIfNecessary
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ū)指出,謝謝~