3、ViewBinding进阶:include标签与ViewBinding、merge标签与ViewBinding、自定义View与ViewBinding、多模块项目中的ViewBinding
好,咱们继续往下走。上一章我们把ViewBinding的基础用法捋了一遍,你可能会觉得:“嗯,不就是个自动生成的绑定类嘛,有啥难的?”
别急,真正的坑往往藏在细节里。这一章我们聊几个进阶场景——include、merge、自定义View,还有多模块。这些场景我当年刚接触时,每个都踩过坑。今天一次性给你讲透。
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 这个类。那怎么办?
两种方案:
- 把merge改成普通布局——比如LinearLayout。但这样会多一层嵌套,影响性能。
- 手动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模块里的布局?
两种方式:
- 通过资源ID引用——在app模块里用
R.layout.lib_layout加载布局,然后手动绑定。但这样你就拿不到library模块里生成的绑定类了。 - 在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