必须使用的绑定组件工具:View Binding

要说在绑定组件这方面,你的第一反应是什么?findViewById?还是Butterknife?再高级一点,Data Binding?接触Kotlin之后发现还学会了Kotlin Synthetics(Kotlin Android Extensions)?

现在,这里要介绍一个全新的绑定组件工具:View Binding,而且这是官方推荐的工具。我在使用过后发现写起来没有那么多冗余代码,而且很顺手,这也就是为什么我说每个写Android程序的人都要使用一下。

那么现在我们来看一下几个问题:它是啥?为什么用它?怎么用

Binding的那些事

说到这,View Binding到底是啥?它是一个将业务代码和视图连接起来的工具。如果想要说得更细一些,那么就要说到什么是绑定(Binding)。

我们都知道,页面上有多个控件,单独将他们放在xml代码中几乎没有交互(注:MotionLayout的动画是一个列外,它可以通过一个xml文件即可实现动画)那么交互写在哪里呢?写在了具体的Activity和Fragment当中。但是你在Activity和Fragment中怎么调用呢?这就是绑定(Binding)所要解决的事情。findViewById(view)这个函数就是干这个用的,但是它有很多问题,例如空指针安全、冗余代码过多等等。因此,大佬Jake Wharton创造了Butterknife来解决这个问题,这个工具一度成为Kotlin出现前最主要的绑定工具。值得一提的是大佬在Kotlin被宣布成Android官方语言之后就去了Google,去年(2020年)中的时候他说他又回到了Square。

Butterknife很好用,通过注解的方式可以直接调用对应的对象进行操作,如果是按钮点击事件的话也可以单独写成一个函数,将这个函数加上注解就可以了,无需设置监听器(Listener)。如果各位去翻看我的BJUTLoginApp项目的3.0版本的话还能看到我使用了这个库。但是使用它的项目在build的时候会很慢,而且有的时候会出现一些奇奇怪怪的问题。

那时候还有一个工具叫做Data Binding,说实话我没怎么使用过,只知道它需要对xml文件加入layout和data字段,然后在对应的TextView中直接使用类中的成员。当时我觉得这东西太复杂了就没有深入的研究过,后来发现Data Binding只有根标签是layout的时候才会生成对应的Binding类,而且build速度上用了annotation processor导致很慢。

到了Kotlin成为Android官方开发语言之后,JetBrains他们搞出了一个叫做Kotlin Android扩展(Kotlin Android Extensions),借用Kotlin语言的一些特性,帮助开发者解决绑定视图。KAE会将xml文件中的每一个有android:id属性的组件都会创建一个对应的对象,对象的名称为该字段的名称,一般情况下都放在了kotlinx.android.synthetic.main.*下面,main后面会接对应的文件名,然后是各个组件。它有一个很严重的问题,如果你引入的包不对会出现NullPointer的错误,而且当你使用RecyclerView的时候也会出现问题。来看下面的例子:

class UserAdapter(var users: List<User> = mutableListOf()) : RecyclerView.Adapter<UserAdapter.UsersViewHolder>() {
    inner class UsersViewHolder(val view: View) : RecyclerView.ViewHolder(view)
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        UsersViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_users, parent, false))
    override fun getItemCount() = users.size
    override fun onBindViewHolder(holder: UsersViewHolder, position: Int) {
        holder.view.user.text = users[position].user
    }
}

这是一个简单的RecyclerView.Adapter的写法,在后面我会说到在View Binding中的通用写法。在KAE中,在onBindViewHolder(holder, position)函数中应该定义每个组件的行为,但是当用到了xml中的组件的时候应该引入哪个包呢?kotlinx.android.synthetic.main.item_users.*?不,是kotlinx.android.synthetic.main.item_users.view.*。因为组件是在ViewHolder中定义的view中使用的,如果此处你采用了前者,那么会出现空指针的错误。也就是说,前者是在Activity、Fragment中直接调用的时候引入,后者是在View对象中使用的时候引入。这个十分容易被混淆。现在,KAE与JetBrains Anko一道,退出了历史的舞台。

View Binding的出现解决了上述的问题,既能保持像KAE的书写优雅,又能像Data Binding这种保证编译类型的安全,同时build速度还很快。下表总结了各个绑定工具优劣:

区别 findViewById Butterknife Data Binding Kotlin Android Extension View Binding
代码优雅程度 ×
编译类型安全 × × ×
Build速度 × ×
双语言支持 ×
空安全 × - ×

注1:KAE从来都没有被Android官方设为推荐使用的绑定工具!但View Binding是。

注2:双语言支持指Java与Kotlin。

View Binding的用法

说了这么多,现在正式开始介绍View Binding!

开启项目对View Binding的支持

View Binding是一个Build Feature,因此你需要在app/build.gradle.kts中的android闭包下添加:

buildFeatures.viewBinding = true

这样,在Gradle build之后工程会根据所有res/layout下的xml文档生成一个对应的类,命名名称是将原文件名的下划线命名法的命名改为帕斯卡命名法的命名,并在末尾加上Binding,例如常见的activity_main.xml会生成ActivityMainBinding

使用View Binding生成的对象

如果你要在Activity、Fragment中使用,也很简单:

  1. 创建binding对象并初始化:
    private val binding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }
    
  1. 在Activity和Fragment中设置View
   // Activity
   override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
     setContentView(binding.root)
     ...
   }
   // Fragment
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
       binding = initBinding(inflater, container)
       return binding.root
   }
   
  1. 使用它吧!你的所有的对象都在binding对象下!
   binding.fab.setOnClickListener{ doSomething() }
   

进阶玩法

RecyclerView中使用

RecyclerView作为最常用的列表控件,它也是可以使用View Binding的,其实写法和KAE差不多,只不过得把ViewHolder中的View换成对应的Binding即可,初始化的话Viewbinding接口中唯一一个函数就是会返回一个View:

inner class UsersAdapter(var users: List<User> = listOf()) : RecyclerView.Adapter<UsersAdapter.UsersViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
            UsersViewHolder(ItemUsersBinding.inflate(layoutInflater))
    ...

    inner class UsersViewHolder(val binding: ItemUsersBinding) : RecyclerView.ViewHolder(binding.root)
}

设计BaseFragment或BaseActivity

为了降低代码的重复率,你可以设计自己的Fragment或者Activity,将他们的共同点都集成在父类中,例如每个Fragment和Activity都会使用到View Binding。

那么你得了解一下View Binding的类们到底是啥,其实它很简单,只是一个接口:

package androidx.viewbinding;

import android.view.View;
import androidx.annotation.NonNull;

/** A type which binds the views in a layout XML to fields. */
public interface ViewBinding {
    /**
     * Returns the outermost {@link View} in the associated layout file. If this binding is for a
     * {@code <merge>} layout, this will return the first view inside of the merge tag.
     */
    @NonNull
    View getRoot();
}

唯一的一个方法就是getRoot(),在Kotlin里会简单写作root属性。在上面的代码中你可以发现Activity只需要binding对象的root,创建的时候虽然只需要Activity自带的layoutInflater,但是它需要调取inflate静态方法,而这个是在生成之后才会有,那问题就好解决了:

abstract class BasicActivity<T : ViewBinding> : AppCompatActivity() {
    val _binding: T? = null
    val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = initBinding
        setContentView(binding.root)
    }

    override fun onDestory() {
        _binding = null
        super.onDestory()
    }
    abstract fun initBinding(): T
}

Fragment也是一样的道理:

abstract class BasicFragment<T : ViewBinding> : Fragment() {
    private var _binding: T? = null
    val binding get() = _binding!!

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        _binding = initBinding(inflater, container)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initViewAfterViewCreated()
    }

    abstract fun initBinding(inflater: LayoutInflater, container: ViewGroup?): T

    abstract fun initViewAfterViewCreated()

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

用的时候只需要多调用一步initBinding(*)即可。

这里也求教一下各位,谁有更简洁的写法欢迎提供一下!