10.1 View Binding基础
10.1.1 findViewById 的演进史
传统开发方式的演进:
| 时代 | 方式 | 优点 | 缺点 |
|---|---|---|---|
| Android 早期 | findViewById | 简单直接 | 类型不安全、运行时错误 |
| ButterKnife | 注解绑定 | 减少样板代码 | 编译时注解、停止维护 |
| Kotlin Synthetics | 插件绑定 | 简洁 | 停止维护、作用域问题 |
| Data Binding | 数据绑定 | 功能强大 | 构建时间长、学习成本高 |
| View Binding | 视图绑定 | 类型安全、轻量 | 不支持数据绑定 |
View Binding 的定位 :
View Binding 是 Android 团队推出的轻量级视图绑定方案,它提供了类型安全的视图访问,同时避免了 Data Binding 的复杂性和性能开销。
10.2 View Binding vs Data Binding vs Kotlin Synthetics
10.2.1 三种方式的对比
详细对比表:
| 特性 | View Binding | Data Binding | Kotlin Synthetics |
|---|---|---|---|
| 类型安全 | ✅ 是 | ✅ 是 | ✅ 是 |
| 编译时检查 | ✅ 是 | ✅ 是 | ❌ 否 |
| 空安全 | ✅ 是 | ✅ 是 | ✅ 是 |
| 数据绑定 | ❌ 否 | ✅ 是 | ❌ 否 |
| 双向绑定 | ❌ 否 | ✅ 是 | ❌ 否 |
| 绑定表达式 | ❌ 否 | ✅ 是 | ❌ 否 |
| Binding Adapters | ❌ 否 | ✅ 是 | ❌ 否 |
| 构建时间 | ⚡ 快 | 🐢 慢 | ⚡ 快 |
| 学习成本 | 🟢 低 | 🟡 中 | 🟢 低 |
| 文件大小 | 🟢 小 | 🟡 中 | 🟢 小 |
| 维护状态 | ✅ 推荐 | ✅ 推荐 | ❌ 已废弃 |
| 性能开销 | 🟢 低 | 🟡 中 | 🟢 低 |
10.2.2 何时使用 View Binding
使用 View Binding 的场景:
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 简单 UI 交互 | ✅ 是 | 只需要访问 View,无需数据绑定 |
| 性能敏感项目 | ✅ 是 | 构建时间快,运行时开销低 |
| 团队快速开发 | ✅ 是 | 学习成本低,上手快 |
| 现有项目迁移 | ✅ 是 | 迁移成本低,无需大幅修改代码 |
| 复杂表单验证 | ❌ 否 | 建议使用 Data Binding |
| 响应式 UI | ❌ 否 | 建议使用 Data Binding |
选择建议:
- 只需要访问 View → View Binding
- 需要数据绑定/响应式 UI → Data Binding
- 学习新技术 → View Binding(更容易上手)
- 现有项目 → View Binding(迁移成本低)
10.3 View Binding 快速入门
10.3.1 启用 View Binding
在 build.gradle 中启用:
gradle
android {
// ...
buildFeatures {
viewBinding true
}
}
模块级别配置:
gradle
// 为所有模块启用
subprojects {
afterEvaluate {
if (it.hasProperty('android')) {
android {
buildFeatures {
viewBinding true
}
}
}
}
}
10.3.2 自动生成的绑定类
绑定类命名规则:
| XML 文件名 | 生成的绑定类名 |
|---|---|
activity_main.xml |
ActivityMainBinding |
fragment_home.xml |
FragmentHomeBinding |
item_product.xml |
ItemProductBinding |
dialog_settings.xml |
DialogSettingsBinding |
绑定类的结构:
kotlin
// 自动生成的 ActivityMainBinding
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final TextView tvTitle;
@NonNull
public final Button btnSubmit;
@NonNull
public final EditText etName;
private ActivityMainBinding(@NonNull ConstraintLayout rootView,
@NonNull TextView tvTitle,
@NonNull Button btnSubmit,
@NonNull EditText etName) {
this.rootView = rootView;
this.tvTitle = tvTitle;
this.btnSubmit = btnSubmit;
this.etName = etName;
}
@Override
@NonNull
public ConstraintLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent,
boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
// 绑定逻辑
}
}
10.3.3 在 Activity 中使用
kotlin
/**
* 主 Activity
*/
class MainActivity : AppCompatActivity() {
// 方式1:使用 lateinit var
private lateinit var binding: ActivityMainBinding
// 方式2:使用 lazy 延迟初始化
// private val binding: ActivityMainBinding by lazy {
// ActivityMainBinding.inflate(layoutInflater)
// }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用 View Binding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupViews()
setupListeners()
}
/**
* 设置视图
*/
private fun setupViews() {
binding.tvTitle.text = "View Binding 示例"
binding.etName.hint = "请输入姓名"
}
/**
* 设置监听器
*/
private fun setupListeners() {
binding.btnSubmit.setOnClickListener {
val name = binding.etName.text.toString()
if (name.isNotEmpty()) {
Toast.makeText(this, "你好,$name!", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "请输入姓名", Toast.LENGTH_SHORT).show()
}
}
}
}
10.3.4 在 Fragment 中使用
kotlin
/**
* 首页 Fragment
*/
class HomeFragment : Fragment() {
// 使用 lazy 延迟初始化
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
setupListeners()
}
/**
* 设置视图
*/
private fun setupViews() {
binding.tvTitle.text = "Home Fragment"
binding.tvDescription.text = "这是一个使用 View Binding 的 Fragment"
}
/**
* 设置监听器
*/
private fun setupListeners() {
binding.btnNavigate.setOnClickListener {
val action = HomeFragmentDirections.actionHomeFragmentToDetailFragment()
findNavController().navigate(action)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 避免内存泄漏
}
}
10.4 View Binding 高级用法
10.4.1 在 RecyclerView Adapter 中使用
kotlin
/**
* 商品 ViewHolder
*/
class ProductViewHolder(
private val binding: ItemProductBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(product: Product, onProductClick: (Product) -> Unit) {
// 设置数据
binding.tvName.text = product.name
binding.tvPrice.text = "¥${product.price}"
binding.tvStock.text = if (product.inStock) "有货" else "缺货"
// 加载图片
Glide.with(binding.ivProduct.context)
.load(product.imageUrl)
.placeholder(R.drawable.placeholder)
.into(binding.ivProduct)
// 设置点击事件
binding.root.setOnClickListener {
onProductClick(product)
}
// 设置库存状态颜色
binding.tvStock.setTextColor(
ContextCompat.getColor(
binding.root.context,
if (product.inStock)
android.R.color.holo_green_dark
else
android.R.color.holo_red_dark
)
)
}
}
kotlin
/**
* 商品 Adapter
*/
class ProductAdapter(
private val onProductClick: (Product) -> Unit
) : RecyclerView.Adapter<ProductViewHolder>() {
private val products = mutableListOf<Product>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val binding = ItemProductBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ProductViewHolder(binding)
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
holder.bind(products[position], onProductClick)
}
override fun getItemCount(): Int = products.size
fun submitList(newProducts: List<Product>) {
products.clear()
products.addAll(newProducts)
notifyDataSetChanged()
}
}
kotlin
/**
* 商品列表 Fragment
*/
class ProductListFragment : Fragment() {
private var _binding: FragmentProductListBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: ProductAdapter
private val viewModel: ProductListViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentProductListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
observeViewModel()
viewModel.loadProducts()
}
/**
* 设置 RecyclerView
*/
private fun setupRecyclerView() {
adapter = ProductAdapter { product ->
viewModel.onProductClick(product)
}
binding.rvProducts.apply {
adapter = this@ProductListFragment.adapter
layoutManager = LinearLayoutManager(requireContext())
addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
}
}
/**
* 观察 ViewModel
*/
private fun observeViewModel() {
viewModel.products.observe(viewLifecycleOwner) { products ->
adapter.submitList(products)
// 显示/隐藏空状态
binding.emptyView.isVisible = products.isEmpty()
binding.rvProducts.isVisible = products.isNotEmpty()
}
viewModel.navigateToProductDetail.observe(viewLifecycleOwner) { event ->
event.getContentIfNotHandled()?.let { product ->
val action = ProductListFragmentDirections
.actionProductListToProductDetail(product.id)
findNavController().navigate(action)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
10.4.2 在 Dialog 中使用
kotlin
/**
* 确认对话框
*/
class ConfirmDialog(
private val title: String,
private val message: String,
private val onConfirm: () -> Unit
) : DialogFragment() {
private var _binding: DialogConfirmBinding? = null
private val binding get() = _binding!!
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogConfirmBinding.inflate(layoutInflater)
// 设置内容
binding.tvTitle.text = title
binding.tvMessage.text = message
// 设置监听器
binding.btnCancel.setOnClickListener {
dismiss()
}
binding.btnConfirm.setOnClickListener {
onConfirm()
dismiss()
}
// 创建对话框
return AlertDialog.Builder(requireContext())
.setView(binding.root)
.create()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
10.4.3 在 BottomSheetDialogFragment 中使用
kotlin
/**
* 底部菜单对话框
*/
class BottomSheetMenuFragment : BottomSheetDialogFragment() {
private var _binding: FragmentBottomSheetMenuBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBottomSheetMenuBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupListeners()
}
/**
* 设置监听器
*/
private fun setupListeners() {
binding.menuProfile.setOnClickListener {
navigateToProfile()
dismiss()
}
binding.menuSettings.setOnClickListener {
navigateToSettings()
dismiss()
}
binding.menuLogout.setOnClickListener {
logout()
dismiss()
}
}
private fun navigateToProfile() {
val action = BottomSheetMenuFragmentDirections.actionBottomSheetMenuToProfile()
findNavController().navigate(action)
}
private fun navigateToSettings() {
val action = BottomSheetMenuFragmentDirections.actionBottomSheetMenuToSettings()
findNavController().navigate(action)
}
private fun logout() {
viewModel.logout()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
10.4.4 在 RecyclerView Item 点击中获取 Binding
kotlin
/**
* 商品 ViewHolder
*/
class ProductViewHolder(
private val binding: ItemProductBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(product: Product, onProductClick: (Product) -> Unit) {
binding.apply {
tvName.text = product.name
tvPrice.text = "¥${product.price}"
Glide.with(ivProduct.context)
.load(product.imageUrl)
.into(ivProduct)
root.setOnClickListener {
onProductClick(product)
}
}
}
}