17、View与ViewGroup KTX:findViewById的KTX替代,ViewBinding与KTX

说实话,做Android开发这么多年,findViewById 这个老朋友我真是又爱又恨。爱的是它简单直接,恨的是它啰嗦又容易出错。尤其是项目大了以后,一个Activity里几十个View,每次都要写 val tvName = findViewById(R.id.tv_name),写到手软。

嗯,今天我们就来聊聊,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 操作的语法糖,比如 doOnLayoutdoOnNextFrame 这些。

真正干掉 findViewById 的,是 ViewBindingKotlin 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 知识结构图

View 与 ViewGroup KTX 知识体系 传统 findViewById • 啰嗦:类型转换 + ID 查找 • 运行时风险:ID 改错不报编译错 • 样板代码多,维护成本高 ViewBinding • 编译期生成绑定类 • 空安全:无 ID 不生成属性 • 类型安全:无需强制转换 KTX 扩展 • doOnLayout • doOnNextFrame • children / forEach 最佳实践:ViewBinding + KTX 组合 binding.root.doOnLayout { ... } binding.children.filterIsInstance<Button>().forEach { ... }

实际项目中的选择建议

说了这么多,到底该怎么选?我根据自己的经验,整理了一个对比表:

方案 优点 缺点 推荐场景
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 的 doOnLayoutchildren 这些扩展函数。遇到需要操作 View 树的地方,优先用 KTX 的链式调用,而不是手写循环。

嗯,这样写出来的代码,既安全又优雅。你试试看?


公众号:蓝海资料掘金营,微信 deep3321