17、View与ViewGroup KTX:findViewById的KTX替代,ViewBinding与KTX
说实话,做Android开发这么多年,findViewById 这个老朋友我真是又爱又恨。爱的是它简单直接,恨的是它啰嗦又容易出错。尤其是项目大了以后,一个Activity里几十个View,每次都要写 val tvName = findViewById,写到手软。
嗯,今天我们就来聊聊,Kotlin和Jetpack是怎么帮我们干掉这个繁琐操作的。
从 findViewById 到 Kotlin 扩展
最早的时候,我们只能用 findViewById。后来有了ButterKnife,再后来Kotlin推出了 Android KTX 中的 View 扩展函数。
我个人习惯把KTX看作是官方给Kotlin开发者开的小灶。它不改变底层API,只是用扩展函数、lambda、默认参数这些Kotlin特性,让调用更自然。
核心思路:用扩展函数替代冗长的 findViewById 调用链。
举个例子,以前我们这样写:
// 传统方式
val button = findViewById<Button>(R.id.my_button)
button.setOnClickListener { ... }
// KTX 方式(其实还是 findViewById,但更简洁)
// 注意:KTX 本身没有直接替代 findViewById,而是提供了更优雅的 View 操作
等等,这里我要纠正一个常见的误解。很多人以为 View KTX 直接提供了 findViewById 的替代品。其实不是的。KTX 主要做的是 View 操作的语法糖,比如 doOnLayout、doOnNextFrame 这些。
真正干掉 findViewById 的,是 ViewBinding 和 Kotlin Android Extensions(虽然它已经被废弃了)。
ViewBinding:官方推荐的替代方案
ViewBinding 是 Jetpack 家族的一员。它会在编译时生成一个绑定类,里面直接包含了所有带 ID 的 View 引用。
我在项目中遇到过好几次这样的场景:一个布局文件有十几个控件,用 findViewById 写了一大堆,结果重构时改了一个ID,编译不报错,运行时直接崩溃。换成 ViewBinding 后,编译期就能发现问题,心里踏实多了。
启用 ViewBinding
首先在 build.gradle 中开启:
android {
...
buildFeatures {
viewBinding = true
}
}
在 Activity 中使用
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 直接使用 binding 对象访问 View
binding.tvTitle.text = "Hello KTX"
binding.btnSubmit.setOnClickListener { ... }
}
}
你看,binding.tvTitle 直接就是一个 TextView 对象,不需要任何类型转换。而且它是 空安全的——如果布局里没有这个ID,编译时就不会生成这个属性。
小技巧:如果你在 Fragment 中使用 ViewBinding,记得在 onDestroyView 中把 binding 置为 null,避免内存泄漏。
ViewBinding + KTX:强强联合
ViewBinding 解决了 View 引用的问题,KTX 解决了 View 操作的问题。两者结合,写出来的代码非常流畅。
举个例子,我想在布局加载完成后执行一个动画:
binding.root.doOnLayout {
// 此时布局已经测量完成
binding.ivAvatar.animate()
.alpha(1f)
.duration(300)
.start()
}
doOnLayout 是 KTX 提供的扩展函数,它会在 View 的 onLayout 回调中执行 lambda。如果没有 KTX,你得写一个 ViewTreeObserver.OnGlobalLayoutListener,还要记得移除监听器,非常麻烦。
我个人特别喜欢 doOnNextFrame 这个函数。它会在下一帧渲染前执行,适合做一些延迟操作:
binding.root.doOnNextFrame {
// 确保 View 已经绘制完成
showTooltip(binding.btnAction)
}
ViewGroup KTX:操作子 View 更顺手
ViewGroup 的 KTX 扩展主要围绕 子 View 的遍历和操作。我以前写自定义布局时,经常要遍历子 View 做统一处理,比如设置相同的点击监听器。
传统写法:
for (i in 0 until viewGroup.childCount) {
val child = viewGroup.getChildAt(i)
if (child is Button) {
child.setOnClickListener { ... }
}
}
用 KTX 的 children 属性:
viewGroup.children
.filterIsInstance<Button>()
.forEach { it.setOnClickListener { ... } }
是不是清爽多了?children 返回的是一个 Sequence,支持链式调用。还有 forEach 扩展函数,可以直接遍历所有子 View。
避坑指南:我曾经在遍历子 View 时直接修改了 ViewGroup 的结构(比如移除子 View),结果抛出了 ConcurrentModificationException。KTX 的 children 本质上是基于索引的遍历,不会触发这个异常,但如果你在遍历中修改结构,还是要小心。
SVG 知识结构图
实际项目中的选择建议
说了这么多,到底该怎么选?我根据自己的经验,整理了一个对比表:
| 方案 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| findViewById | 无额外依赖,简单 | 啰嗦,运行时风险 | 极小的 Demo 项目 |
| Kotlin Android Extensions | 语法最简洁(已废弃) | 全局导入,容易命名冲突,已不维护 | ❌ 不推荐新项目使用 |
| ViewBinding | 编译期安全,空安全,类型安全 | 每个布局生成一个类,略微增加编译时间 | ✅ 所有新项目首选 |
| ViewBinding + KTX | 安全 + 流畅的链式调用 | 需要同时引入两个依赖 | ✅ 强烈推荐,尤其适合复杂 UI |
注意:Kotlin Android Extensions 插件在 Kotlin 1.8 之后已经被正式移除。如果你还在用 kotlinx.android.synthetic,请尽快迁移到 ViewBinding。我曾经在一个老项目里看到几百个文件都用了 synthetic,迁移时改到怀疑人生……
总结
说白了,findViewById 的时代已经过去了。ViewBinding 提供了编译期的安全保障,KTX 提供了运行时的语法糖。两者结合,写 UI 代码就像在写 Kotlin 标准库一样自然。
我个人现在的习惯是:新项目一律用 ViewBinding,然后配合 KTX 的 doOnLayout、children 这些扩展函数。遇到需要操作 View 树的地方,优先用 KTX 的链式调用,而不是手写循环。
嗯,这样写出来的代码,既安全又优雅。你试试看?