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