關鍵詞:Kotlin 1.4 KAEandroid
本文假定你們瞭解 KAE(Kotlin Android Extensions)。git
前幾天看到郵件說 Kotlin 1.4.20-M2(https://github.com/JetBrains/kotlin/releases/tag/v1.4.20-M2) 發佈了,因而打開看了看更新,發現有個新的用於 Parcelize 的插件。要知道這個功能一直都是集成在 KAE 當中的,那 KAE 呢?github
緊接着咱們就能夠看到一行:Deprecate Kotlin Android Extensions compiler plugin(https://youtrack.jetbrains.com/issue/KT-42121)。web
說實話,直接廢棄,我仍是有些意外的。畢竟這個插件在早期爲 Kotlin 攻城略地快速吸引 Android 開發者立下了汗馬功勞,多年來雖然幾乎沒有功能更新,但直到如今仍然可以勝任絕大多數場景。面試
非要說廢棄的理由,確實也能羅列幾個出來。爲了方便,咱們把以 layout 當中 View 的 id 爲名而合成的屬性簡稱合成的屬性。緩存
銷燬以後的空指針
KAE 是經過在字節碼層面添加合成屬性來解決 findViewById 的問題的,對於 Activity 和 Fragment 而言,合成的屬性背後其實就是一個緩存,這個緩存會在 Activity 的 onDestroy、Fragment 的 onDestroyView 的時候清空。因此每次訪問合成的屬性,其實只有第一次是調用 findViewById,以後就是一個查緩存的過程。安全
這個設計很合理,不過也難免有些危險存在。主要是在 Fragment 當中,若是不當心在 onDestroyView 調用以後訪問了這些合成的屬性,就會拋一個空指針異常,由於此時緩存已經被清空,而 Fragment 的 View 也被置爲 null 了。微信
...
import kotlinx.android.synthetic.main.activity_main.*
class MainFragment : Fragment() {
...
override fun onDestroyView() {
super.onDestroyView()
textView.text = "Crash!"
}
}
必須說明的一點是,這裏拋空指針是合理的,畢竟 Fragment 的 View 的生命週期已經結束了,不過生產實踐當中不少時候不是一句「合理」就能解決問題的,咱們要的更多的是給老闆減小損失。這裏若是 textView 仍然能夠訪問,它不過是修改了一下文字而已,不會有其餘反作用,但偏偏由於 KAE 這裏嚴格的遵照了生命週期的變化清空了緩存,卻又沒有辦法阻止開發者繼續訪問這個合成屬性而致使空指針。對比而言,若是咱們直接使用 findViewById,狀況多是下面這樣:app
lateinit var textView: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textView = view.findViewById(R.id.textView)
}
override fun onDestroyView() {
super.onDestroyView()
textView.text = "Nothing happened."
}
這樣的代碼雖然看上去不怎麼高明,但它至少不會 Crash。框架
Kotlin 一貫追求代碼的安全性,並且但願在編譯時就把代碼運行時可能產生的問題儘量地暴露出來。在不少場景下 Kotlin 確實作得很好,然而 KAE 並無作到這一點。
就這個具體的問題而言,倒也很容易解決,如今 Android 當中已經有了足夠多的生命週期管理工具,咱們可以很好的避免在 Fragment 或者 Activity 的生命週期結束以後還要執行一些相關的操做。例如使用 lifecycleScope.launchWhenResumed{ ... }
就能很好的解決這個問題。
這麼看來,這一點彷佛不算是 KAE 自己的缺陷。難道是咱們要求過高了?不,下降標準的事兒咱們是毫不會作的,Kotlin 官方這麼多年都沒有解決這個問題,快出來捱打 (╬ ̄皿 ̄)=○#( ̄#)3 ̄) 。
張冠李戴
因爲合成的屬性只能從 Receiver 的類型上作限制,沒法肯定對應的 View、Activity、Fragment 當中是否真實存在這個合成的屬性對應 id 的 View,所以也存在訪問安全性上的隱患。
例如我當前的 Activity 的 layout 是 activity_main.xml,其中並未定義 id 爲 textView 的 View,然而下面的寫法卻不會在編譯時報錯:
import kotlinx.android.synthetic.main.fragment_main.*
...
textView.text = "MainActivity"
編譯時高高興興,運行時就要垂頭喪氣了,由於 findViewById 必定會返回 null,而合成的屬性又不是可空類型。
這個問題從現有的 KAE 的思路上來看,確實不太好解決,不過從多年的實踐來看,這也許都算不上是一個問題,至少我用了快 5 年 KAE,只有偶爾幾回寫錯 id 之外,多數狀況下不會出現此類問題。這個問題確實算是一個缺陷,但它的影響實在是有限。
衝突的 ID
還有一個問題就是命名空間的問題。合成的屬性從導包的形式上來看,像是以 layout 的文件名加上固定的前綴合成的包下的頂級屬性,一旦這個包被導入,當前的整個文件當中均可以使用 View、Activity、Fragment 來訪問這些合成的屬性,這就及其容易致使命名空間衝突的問題。
爲了說明問題,咱們建立兩個徹底相同的 layout,分別命名爲 view_tips.xml 和 view_warning.xml,裏面只是簡單的包含一個 id 爲 textView 的 TextView
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
而後在 Activity 或者 Fragment 當中加載這兩個 layout:
val tipsView = View.inflate(view.context, R.layout.view_tips, null)
val warningView = View.inflate(view.context, R.layout.view_warning, null)
tipsView.textView.text = "Tips"
warningView.textView.text = "Warning"
... // 添加到對應的父 View 當中
那麼這時候咱們就要面臨一個導包的問題,tipsView 和 warningView 訪問的合成屬性可能來自於如下兩個包:
kotlinx.android.synthetic.main.view_tips.view.*
kotlinx.android.synthetic.main.view_warning.view.*
咱們固然能夠把兩者一併導入,但問題在於兩者即使如此,合成的屬性在編譯時靜態綁定也只能綁定到一個包下面的合成屬性下,這樣的結果就是咱們在 Android Studio 當中點擊 warningView.textView 可能會跳轉到 view_tips 這個 layout 當中。
運行時會不會有問題呢?那倒不至於,由於你始終記住合成屬性在運行時會替換成 findViewById 就能夠了,只要 findViewById 不出問題,那合成屬性天然也不存在問題。從生成的字節碼來看,warningView.textView
其實就等價於 warningView.findViewById(R.id.textView)
:
ALOAD 4
DUP
LDC "warningView"
GETSTATIC com/bennyhuo/helloandroid/R$id.textView : I
INVOKEVIRTUAL android/view/View.findViewById (I)Landroid/view/View;
CHECKCAST android/widget/TextView
因此這個問題本質上影響的是開發體驗。出現衝突,一方面多是類文件太大,包含的 UI 邏輯過多,致使引入過多的 layout,從而產生衝突;另外一方面也多是佈局上拆分得過小,一個視圖的邏輯類當中不得不引入大量的 layout 致使衝突。經過合理的設計 UI 相關的類,這個問題自己也能夠很好的規避。
另外,若是語言自己支持把包名做爲命名空間,在代碼訪問時直接予以限定,同樣能夠達到目的。按照現有的語法特性,若是合成的屬性是在一個 object 當中定義:
object ViewTipsLayout {
val View.textView: TextView
get() = findViewById(R.id.textView)
}
object ViewWarningLayout {
val View.textView: TextView
get() = findViewById(R.id.textView)
}
那麼使用的時候若是產生 id 衝突,就能夠這樣:
with(ViewTipsLayout) {
tipsView.textView.text = "Tips"
}
with(ViewWarningLayout) {
warningView.textView.text = "Warning"
}
固然,這只是咱們的設想了。畢竟都要廢棄了。
不支持 Compose
去年的時候 Anko 就被廢棄了,這麼想來,KAE 能苟活這麼久大概是由於根本不怎麼須要維護吧?在這裏提 Anko 到不是爲了嘲諷,Anko 雖然離開了咱們,可 Anko 所倡導的 DSL 佈局的精神卻留了下來,也就是 Jetpack 當中仍然處於 Alpha 狀態(怎麼都是 Alpha,難道這麼久了還不配有個 Beta 嗎)的 Compose 了。
Anko Layout 不算成功,主要緣由仍是開發成本的問題。預覽要等編譯,編譯又要好久,這簡直了,誰用誰知道。隔壁家的 SwiftUI 就作得很好,說明魚和熊掌仍是能夠兼得的,因此我看好 Compose,就看 Android 還能活幾年,能不能等到那個時候了(哈哈哈,開玩笑)。
Kotlin 最近一直在推 KMM,你們都在猜 Kotlin 官方會不會搞一個 React Kotlin Native 或者 Klutter 出來,結果最近咱們就看到 JetBrains 的 GitHub 下一個叫 skiko(https://github.com/JetBrains/skiko) 的框架很是活躍,它是基於 Kotlin 多平臺特性封裝的 Skia 的 API(Flutter:喵喵喵??)。還有一個就是 compose-jb(https://github.com/JetBrains/compose-jb) 了,我粗略看了下,目前已經把 Compose 移植到了桌面上,支持了 Windows、Linux、macOS,也不知道 iOS 被安排了沒有(真是司馬昭之心啊)。因此 Compose 已經再也不是 Android 的了,它是你們的。
對於 Compose 而言,KAE 一點兒用都沒有,由於人家根本不須要作 View 綁定好很差。
KAE:我這麼優秀!
Compose:你給我讓開!
使用 ViewBinding 做爲替代方案
那麼問題來了,KAE 廢棄以後會怎麼樣呢?按照連接當中的說明來看,廢棄以後仍然可使用,但會有一個警告;固然,出現問題官方也不會再修復了,更不會有新功能。
Kotlin 官方建議開發者使用 Android 的 View Binding(https://developer.android.com/topic/libraries/view-binding) 來解決此類場景的問題。客觀的講 View Binding 確實能解決前面提到的幾個 KAE 存在的問題,但 View Binding 的寫法上也會略顯囉嗦:
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
}
訪問 View 時:
binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }
相比之下,KAE 解決了 findViewById 的類型安全和訪問繁瑣的問題;而 View Binding 則在此基礎上又解決了空安全的問題。
我看到在廢棄 KAE 的討論中,你們仍是以爲廢棄有些難以理解,畢竟以前你也沒怎麼管這個插件啊,這麼多年了除了加了個 Parcelize 的功能之外,也沒怎麼着啊。不過歷史的車輪老是在往前滾((ノ`Д)ノ)的嘛, Kotlin 官方這麼急着廢棄 KAE,也許就是要爲 View Binding 讓路,JetBrains 如今和 Google 穿一條褲子,誰知道他們是否是有什麼對將來的美(si)好(xia)規(jiao)劃(yi)呢?哈哈,玩笑啦。
其實 View Binding 除了寫起來多了幾行代碼之外,別的倒也沒什麼大毛病。而寫法複雜這個嘛,其實說來也簡單,咱們稍微封裝一下不就好了麼?
abstract class ViewBindingFragment<T: ViewBinding>: Fragment() {
private var _binding: T? = null
val binding: T
get() = _binding!!
abstract fun onCreateBinding(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): T
abstract fun T.onViewCreated()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return onCreateBinding(inflater, container, savedInstanceState).also {
_binding = it
}.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.onViewCreated()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
這樣用的時候直接繼承這個類就行了:
class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
override fun onCreateBinding(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): FragmentMainBinding {
return FragmentMainBinding.inflate(inflater, container, false)
}
override fun FragmentMainBinding.onViewCreated() {
textView.text = "MainFragment"
textView.setOnClickListener {
Toast.makeText(requireContext(), "Clicked.", Toast.LENGTH_SHORT).show()
}
}
}
這個也就是我隨手那麼一寫,確定算不上完美,但至少說明 View Binding 的寫法同樣能夠作到很簡潔。
小結
KAE 本質上就是經過編譯器生成字節碼的方式爲 Activity、Fragment、View 提供了以 xml 佈局中的 id 爲名的合成屬性,從而簡化使用 findViewById 來實現 View 綁定的一個插件。
相比之下,KAE 比 findViewById 自己提供了更簡便的 方式,也保證了 View 的類型安全,但卻沒法保證 View 的空安全 —— 而這些問題都在 ViewBinding 當中獲得瞭解決。
無論怎樣,KAE 被廢棄是沒什麼懸念了,它曾經一度填補了 Android 開發體驗上的空缺,也曾經一度受到追捧和質疑,更曾是 Kotlin 早期吸引 Android 開發者的一把利器,如今終於完成了它本身的歷史任務。
再見,KAE。
Kotlin 協程對大多數初學者來說都是一個噩夢,即使是有經驗的開發者,對於協程的理解也仍然是懵懵懂懂。若是你們有一樣的問題,不妨閱讀一下個人新書《深刻理解 Kotlin 協程》,完全搞懂 Kotlin 協程最難的知識點:
若是你們想要快速上手 Kotlin 或者想要全面深刻地學習 Kotlin 的相關知識,能夠關注我基於 Kotlin 1.3.50 全新制做的新課,課程初版曾幫助3000多名同窗掌握 Kotlin,此次更新迴歸內容更精彩:
掃描二維碼便可進入課程啦!
Android 工程師也能夠關注下《破解Android高級面試》,這門課涉及內容均非淺嘗輒止,除知識點講解外更注重培養高級工程師意識,目前已經有 1100 多位同窗在學習:
掃描二維碼便可進入課程啦!
本文分享自微信公衆號 - Kotlin(KotlinX)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。