3、ViewBinding进阶:include标签与ViewBinding、merge标签与ViewBinding、自定义View与ViewBinding、多模块项目中的ViewBinding

好,咱们继续往下走。上一章我们把ViewBinding的基础用法捋了一遍,你可能会觉得:“嗯,不就是个自动生成的绑定类嘛,有啥难的?”

别急,真正的坑往往藏在细节里。这一章我们聊几个进阶场景——include、merge、自定义View,还有多模块。这些场景我当年刚接触时,每个都踩过坑。今天一次性给你讲透。

ViewBinding 进阶知识体系 ViewBinding 进阶 include 标签 merge 标签 自定义 View 多模块项目 需显式绑定 include 布局 merge 不生成绑定类 需手动 inflate 模块间不共享绑定类 核心原则:每个布局文件独立生成绑定类

3.1 include标签与ViewBinding

先说说include。这是Android布局复用最常用的手段。但用ViewBinding时,很多人会问:“include进来的布局,我该怎么拿到里面的控件?”

直接回答:include进来的布局,会作为一个独立的绑定类对象暴露出来

举个例子。假设你有一个公共的头部布局 header_layout.xml

<!-- header_layout.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btn_back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="返回" />
</LinearLayout>

然后在主布局 activity_main.xml 中include它:

<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include
        android:id="@+id/header"
        layout="@layout/header_layout" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

注意看,我给include标签加了一个 android:id="@+id/header"。这个id很关键。有了它,生成的 ActivityMainBinding 里就会多出一个 HeaderLayoutBinding 类型的字段:

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 通过 header 字段访问 include 布局里的控件
        binding.header.tvTitle.text = "我的标题"
        binding.header.btnBack.setOnClickListener { finish() }
    }
}

关键点:include标签必须设置android:id,否则生成的绑定类中不会暴露该include布局的绑定对象。你只能通过 binding.root.findViewById 去手动查找——那还不如不用ViewBinding呢。

我的习惯:只要用include,我一定给include标签加id。哪怕布局里只有一个控件,我也加。这样代码可读性高很多,别人一看就知道“哦,这里引用了header布局”。

3.2 merge标签与ViewBinding

merge标签就有点特殊了。它本身不生成任何绑定类。

为什么会这样?因为merge标签的作用是“把子布局直接合并到父布局中”,它没有自己的根节点。ViewBinding是基于布局文件的根节点来生成绑定类的——根节点都没了,自然生成不了。

举个例子。假设你有一个 item_product.xml

<!-- item_product.xml -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/tv_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</merge>

你会发现,不会生成 ItemProductBinding 这个类。那怎么办?

两种方案:

  1. 把merge改成普通布局——比如LinearLayout。但这样会多一层嵌套,影响性能。
  2. 手动inflate,然后自己绑定——用 View.inflate() 或者 LayoutInflater 加载,然后手动findViewById。

我个人更推荐第一种。除非你特别在意那一层嵌套的性能损耗,否则用LinearLayout代替merge,换来ViewBinding的便利,是值得的。

我曾经踩过的坑:在一个RecyclerView的item布局里用了merge,结果发现ViewBinding死活不生成类。查了半天文档才明白——merge不参与绑定。后来我改成LinearLayout,世界清净了。

3.3 自定义View与ViewBinding

自定义View是另一个容易让人困惑的地方。你写了一个自定义View,里面有自己的布局文件,怎么用ViewBinding?

直接说结论:在自定义View的构造函数中手动inflate绑定类

来看代码。假设你有一个 CustomCardView,布局文件是 view_custom_card.xml

class CustomCardView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private val binding: ViewCustomCardBinding

    init {
        // 手动 inflate 绑定类
        binding = ViewCustomCardBinding.inflate(
            LayoutInflater.from(context), this, true
        )
        // 现在可以直接用 binding 访问控件了
        binding.tvTitle.text = "默认标题"
    }

    fun setTitle(title: String) {
        binding.tvTitle.text = title
    }
}

注意这里的关键点:

  • ViewCustomCardBinding.inflate() 的第三个参数传 true,表示把inflate出来的布局直接添加到当前ViewGroup(也就是this)中。
  • 如果你传 false,就需要手动 addView(binding.root)

我建议:在自定义View里,把binding声明为类的属性,而不是局部变量。这样整个类的其他方法都能方便地访问控件。我见过有人每个方法里都写一遍 binding.tvTitle,其实没必要——声明成属性,一劳永逸。

3.4 多模块项目中的ViewBinding

多模块项目是大型App的标配。ViewBinding在多模块下表现如何?

答案是:每个模块独立生成自己的绑定类,模块之间不共享

举个例子。你的项目有 app 模块和 library 模块。library模块里有一个 lib_layout.xml,它只会生成 LibLayoutBinding 在library模块的build目录下。app模块是看不到这个类的。

那app模块怎么用library模块里的布局?

两种方式:

  1. 通过资源ID引用——在app模块里用 R.layout.lib_layout 加载布局,然后手动绑定。但这样你就拿不到library模块里生成的绑定类了。
  2. 在library模块中暴露绑定方法——比如library模块提供一个工具方法,返回绑定好的View。

我个人更推荐第二种。来看代码:

// library 模块中
object LibViewBinder {
    fun bindLibLayout(inflater: LayoutInflater, parent: ViewGroup?): LibLayoutBinding {
        return LibLayoutBinding.inflate(inflater, parent, false)
    }
}

然后在app模块中:

val binding = LibViewBinder.bindLibLayout(layoutInflater, container)
// 现在可以访问 library 布局里的控件了
binding.tvLibTitle.text = "来自library模块"

一个小技巧:如果你在library模块里定义了自定义View,并且这个View内部用了ViewBinding,那app模块在使用这个自定义View时,完全不需要关心它的内部实现。你只需要在XML里声明这个View,它自己会处理好一切。这就是封装的好处。

注意:多模块下,如果library模块的build.gradle里没有开启ViewBinding,那它就不会生成任何绑定类。app模块自然也无法使用。所以记得在每个模块的build.gradle里都加上 buildFeatures { viewBinding = true }

好了,这一章的内容就这些。include、merge、自定义View、多模块——这四个场景覆盖了ViewBinding进阶的大部分痛点。你想想看,是不是每个场景都有对应的解决方案?

下一章我们会聊DataBinding的基础,那又是另一个世界了。不过别担心,有了ViewBinding的基础,学DataBinding会轻松很多。


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