关键词: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源创计划”,欢迎正在阅读的你也加入,一块儿分享。