Android解放双手的利器之ViewBinding

1. 背景

写代码最繁琐的是什么?重复的机械操作。我们刚接触Android开发时最常写的操作肯定少不了findViewById 的身影。如果页面简单,负担还好,多写几行而已,但如果界面中存在几十上个View呢?再或者,重复的做一件枯燥的事几百次呢?这时候就会敲代码敲到手抽筋,有点生无可恋了吧。

表情包

2. ViewBinding是什么

这时候你的救星它来了!解放你的双手,效率提升十倍!它就是 ViewBinding !

ViewBinding ,顾名思义是“视图绑定”。它可以自动为 XML 布局文件生成一个绑定类,通过这个绑定类,你可以直接拿到布局中的View,再也不用 findViewById 的一个个去找了。

ViewBinding 是AndroidStudio3.6以后就支持的功能,现在大家的Android Studio版本应该都是202x.x.x这种新的年月日版本了吧。Android Studio4.2.2之后就采用这种新版本命名法了。

3. 开启ViewBinding功能

3.1 前置条件

Android Studio版本:>=3.6
Gradle版本:>3.6

3.2 配置开启viewBinding

不同Gradle 版本,开启viewBinding的方式有差别:

  • Gradle 版本 4.1 以上

    在 module级别的 build.gradle 文件中,添加如下代码:

    android {
        ...
        buildFeatures {
            viewBinding true
        }
    }
    

    如果你的 build.gradle 是 build.gradle.kts 这种文件,则这样添加代码:

    android {
        ...
        buildFeatures {
            viewBinding = true
        }
    }
    
  • Gradle 版本 4.1 以下
    如果你的 Gradle 版本 非常旧(建议最好还是用比较新的,别用这种低版本了),则配置语法有所不同:

    viewBinding {
        enabled = true
    }
    

4. 生成绑定类

添加配置代码之后,会提示你点击 sync 同步代码,然后 build 一下工程 AS 会自动为你的工程生成绑定类代码,目录在app/build/generated/data_binding_xxx下。

生成的绑定类命名规则是,将 XML 文件的名称转换为“驼峰命名法”的形式,并在末尾添加“Binding”一词。比如,你的布局文件名是:activity_main.xml,那么生成的绑定类名是: ActivityMainBinding

绑定代码生成

默认情况下,AS会对工程中的所有xml文件生成绑定类。如果不想为某个布局文件生成,则可以将 tools:viewBindingIgnore=“true” 属性添加到该布局文件的根视图中,例如:👇

<LinearLayout
        ...
        tools:viewBindingIgnore="true" >
    ...
</LinearLayout>

5. 使用ViewBinding

ViewBinding可以用在各种需要布局与代码交互的地方,如Activity、Fragment、ViewHolder等

5.1 Activity 中使用

首先是布局,不需要做任何修改😄,比如布局:activity_main.xml👇:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="100dp"
        android:layout_height="100dp" />

    <Button
        android:id="@+id/btn_ok"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="按钮" />
</LinearLayout>
  • 使用 ViewBinding 前
    MainActivity.kt 👇:

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.widget.Button
    import android.widget.ImageView
    import android.widget.TextView
    
    class MainActivity : AppCompatActivity() {
    
        private var tvTitle: TextView? = null
        private var ivIcon: ImageView? = null
        private var btnOk: Button? = null
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            tvTitle = findViewById(R.id.tv_title)
            ivIcon = findViewById(R.id.iv_icon)
            btnOk = findViewById(R.id.btn_ok)
            tvTitle?.text = "你好"
            ivIcon?.setBackgroundColor(Color.RED)
            btnOk?.text = "确认"
            btnOk?.setOnClickListener { 
                Toast.makeText(this@MainActivity, "点击了", Toast.LENGTH_SHORT).show()
            }
        }
    }
    
  • 使用 ViewBinding 后
    MainActivity.kt 👇:

    import android.graphics.Color
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.widget.Toast
    import com.example.mtes.databinding.ActivityMainBinding
    
    class MainActivity : AppCompatActivity() {
    
        private var activityMainBinding: ActivityMainBinding? = null
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(activityMainBinding?.root)
            activityMainBinding?.tvTitle?.text = "你好"
            activityMainBinding?.ivIcon?.setBackgroundColor(Color.RED)
            activityMainBinding?.btnOk?.text = "确认"
            activityMainBinding?.btnOk?.setOnClickListener {
                Toast.makeText(this@MainActivity, "点击了", Toast.LENGTH_SHORT).show()
            }
        }
    }
    

到这里,相信你已经学会了 ViewBinding 的基本用法,体会到它的便捷了吧,再见了 findViewById 😄。

5.2 Fragment 中使用

这里就省略布局的举例了,跟Activity一样,拿到binding后,直接点出来你想访问的view即可。相信聪明的你一定能理解👋

  • 使用 ViewBinding 前,MyFragment.kt 👇:
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

class MyFragment : Fragment() {
   
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_my, container, false)
    }
}
  • 使用 ViewBinding 后,MyFragment.kt 👇:

    import android.os.Bundle
    import androidx.fragment.app.Fragment
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import com.example.mtes.databinding.FragmentMyBinding
    
    class MyFragment : Fragment() {
    
        private var binding: FragmentMyBinding? = null
    
        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            binding = FragmentMyBinding.inflate(layoutInflater, container, false)
            val view = binding?.root
            return view
        }
        
    	override fun onDestroyView() {
           super.onDestroyView()
    	        binding = null
    	   }
    
    }
    

注意Fragment中使用 ViewBinding 有内存泄漏的风险。比如Fragment的替换操作时会执行 onDestroyView 方法,但不会onDestroy,这时Fragment会回收根View,但整个View树被我们生成的 ViewBinding 类对象持有,导致无法回收,从而导致了内存泄漏。

为解决这一风险问题,应在Fragment的 onDestroyView 方法中手动将 binding 对象置空,从而避免内存泄漏

override fun onDestroyView() {
	       super.onDestroyView()
	        binding = null
	   }

5.3 ViewHolder 中使用

列表也是我们常见的场景,使用 RecyclerView + Adapter + ViewHolder 实现列表,里面少不了对ViewHolder中的View的 findViewById。

比如列表item布局: layout_holder.xml 👇

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/tv_item"
        />
</LinearLayout>
  • 使用ViewHolder前,MyAdapter.kt👇:

    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.TextView
    import androidx.recyclerview.widget.RecyclerView
    
    class MyAdapter: RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
    
        private val dataList: List<String>? = null
    
        class MyViewHolder(val itemView: View) : RecyclerView.ViewHolder(itemView) {
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_holder, parent, false)
            return MyViewHolder(view)
        }
    
        override fun getItemCount(): Int {
            return dataList?.size ?: 0
        }
    
        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            val tvText: TextView = holder.itemView.findViewById<TextView>(R.id.tv_item)
            tvText.text = "你好"
        }
    }
    
  • 使用ViewHolder后,MyAdapter.kt👇:

    import android.view.LayoutInflater
    import android.view.ViewGroup
    import androidx.recyclerview.widget.RecyclerView
    import com.example.mtes.databinding.LayoutHolderBinding
    class MyAdapter: RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
    
        private val dataList: List<String>? = null
    
        class MyViewHolder(val binding: LayoutHolderBinding) : RecyclerView.ViewHolder(binding.root) {
    
        }
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val binding = LayoutHolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
            return MyViewHolder(binding)
        }
    
        override fun getItemCount(): Int {
            return dataList?.size ?: 0
        }
    
        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            holder.binding.tvItem.text = "标题"
        }
    }
    

6. ViewBinding的优点

与使用 findViewById 相比,视图绑定具有一些很显著的优点:

  • Null 安全
    由于 ViewBinding 会创建对布局中的 view 的直接引用,因此不存在因 view ID 找不到而引发 null 指针异常的风险.
  • 类型安全
    viewbinding 生成的属性类型和布局中的View类型是一致的,不存在之前findViewById后,类型转换的风险。

7. 与 dataBinding 对比

dataBinding 与 viewBinding 类似,也是可以生成绑定代码,但侧重点在于对于数据的赋值,相比 viewBinding 更复杂一些。因此如果只是想省略 findViewById ,那么推荐用 viewBinding 就好了,因为它有以下优势:

  • 加快编译速度:视图绑定不需要处理注解,因此编译时间更短。
  • 易于使用:视图绑定不需要特别标记的 XML 布局文件,因此在您的应用中采用的速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。

另一方面,与 dataBinding 相比,viewBinding也有一些劣势:

  • viewBinding 不支持布局变量或布局表达式,因此它不能用于直接从 XML 布局文件声明动态界面内容。
  • viewBinding 不支持双向数据绑定。

最后,dataBinding 与 viewBinding 是可以同时使用的,在哪个页面用哪个,取决于你喽🤣。

怎么样,有了 ViewBinding,妈妈再也不用担心你的手指了,快去试试吧~
如果这篇文章对你有用,欢迎支持🙏

8. 特殊情况

8.1 布局中嵌套include标签

上述我们使用 ViewBinding 时都是在普通的一个布局文件中,不存在多个布局嵌套,所以可以根据生成的代码直接引用到布局中的id。

如果布局中存在嵌套,比如使用 include 标签引用了另一个布局,这时就没法直接用XXXbinding对象去引用嵌套布局里的id了。

这时该怎么引用呢?🤔

💡!也比较容易,多点一层调用就行了!😄。

先说下解决方法:

  1. 为 include 标签添加 id ;
  2. 使用 binding 访问到 include 节点,再访问到 include节点内部的其他控件。

再举例说明下:还是上面的 Activity 中,有下面的布局,activity_main.xml:⬇️

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <include
        android:id="@+id/include_title_bar"
        layout="@layout/title_bar" />
    
    <TextView
        android:id="@+id/tv_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="100dp"
        android:layout_height="100dp" />

    <Button
        android:id="@+id/btn_ok"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="按钮" />
</LinearLayout>

注意 上述 include 标签 引用了 一个标题布局: title_bar.xml,并且我们 为 include 标签添加了 idandroid:id="@+id/include_title_bar"

title_bar.xml:⬇️

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:gravity="center">

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

</LinearLayout>

在 Activity 代码中访问

MainActivity: ⬇️

import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import com.example.mtes.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private var activityMainBinding: ActivityMainBinding? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(activityMainBinding?.root)
        
        // 访问当前布局中的控件
        activityMainBinding?.ivIcon?.setBackgroundColor(Color.RED)
        activityMainBinding?.btnOk?.text = "确认"
        activityMainBinding?.btnOk?.setOnClickListener {
            Toast.makeText(this@MainActivity, "点击了", Toast.LENGTH_SHORT).show()
        }
        // 访问 include 中的控件 tvtitle
        activityMainBinding?.includeTitleBar?.tvTitle?.text = "你好"

    }
}

可以看到,访问 include 中的控件 tvtitle,只需要给include标签添加ID,然后就能引用到include节点,再点到里面的控件了,像这样:activityMainBinding?.includeTitleBar?.tvTitle

这就是 ViewBinding 处理嵌套布局的方法了。

官网文档:https://developer.android.google.cn/topic/libraries/view-binding#groovy

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

子林Android

感谢老板,老板大气!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值