ViewBinding的使用及原理

ViewBinding的使用及原理

背景

1.findViewById

作为 Android 开发人员,没有不知道 findViewById 的。

想要操作视图中的控件,都需要先用到 findViewById 来找到这个控件,然后就可以对齐进行一系列的操作了(如:改变属性、设置监听、添加动画)。

缺点:当 xml 视图中控件较多时,代码中的 findViewById 也相应的越来越多。

2.ButterKnife

通过注解的方式来完成布局文件的绑定,从而避免写 findViewById 来提高效率。

示例:

@BindView(R.id.tv_content)
TextView tvContent;

缺点:(其实也不能算缺点,只是时代在发展)在如今组件化随处可见的时期,使用 ButterKnife 就有点麻烦了,在 library 中需要将 R 修改为 R2 才可,这时候当 module 在 application 和 library 切换时就比较麻烦了。

3.kotlin-android-extensions

作为很多人初始接触 Kotlin 的一个入门操作,最初都是因为使用 kotlin-android-extensions 简便而用上了 Kotlin。

使用确实很简单,在工程 build.gradle 文件中添加

dependencies{
    classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
}

然后在 module 的 build.gradle 中添加

apply plugin: 'kotlin-android-extensions'

这时候就直接可以使用 xml 视图中的控件 id 来操作控件了

tv_content.text = "Smaple"

缺点:
1.在模块之间不能通用,比如:业务模块中不能使用基础模块中的基础布局
2.由于不同的资源文件可以使用相同的控件 id 名称,会有一定的错误可能性。

由于上面的原因,所以有了 ViewBinding ,后面还有 DataBinding。
关于 DataBinding 可以参考笔者的这篇介绍:

ViewBinding 的使用

添加配置在 module 的 build.gradle

android{
    buildFeatures{
        viewBinding true
    }
}

之后在编译的时候就会为我们的所有 xml 文件生成对应的 Binding 类了,如:activity_main.xml 对应的 ActivityMainBinding。

使用也非常简单,Activity 如下:

class MainActivity : AppCompatActivity() {

    private lateinit var activityMainBinding: ActivityMainBinding

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

        activityMainBinding.tv_content = "Smaple"
    }
}

Fragment 如下:

class MainFragment: Fragment() {

    private var fragmentMainBinding: FragmentMainBinding? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        fragmentMainBinding = FragmentMainBinding.inflate(inflater, container, false)
        return super.onCreateView(inflater, container, savedInstanceState)
    }

    // 注意:在这里将 Binding 对象置为 null,防止内存泄漏(因为 Fragment 存在的时间大于视图)
    override fun onDestroyView() {
        super.onDestroyView()
        fragmentMainBinding = null
    }

}

如果某个布局不想使用 ViewBinding ,也就是不生成对应的 xxxBinding 类,那么只需要在 xml 的根 View 中添加 tools:viewBindingIgnore="true"

ViewBinding 原理

原理追溯到本质上还是使用了 findViewById 来实现了绑定。

只不过这个过程是通过 gradle 插件来实现的,在编译的时候 gradle 插件会为每一个 xml 布局生成对应的 xxxBinding 类,当然如果某个 xml 中配置了 tools:viewBindingIgnore="true" 的话就会被插件跳过了。

ActivityMainBinding.java 源码如下:

public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final RelativeLayout rootView;

  @NonNull
  public final TextView tvContent;

  private ActivityMainBinding(@NonNull RelativeLayout rootView, @NonNull TextView tvContent) {
    this.rootView = rootView;
    this.tvContent = tvContent;
  }

  @Override
  @NonNull
  public RelativeLayout 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) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    int id;
    missingId: {
      id = R.id.tv_content;
      TextView tvContent = ViewBindings.findChildViewById(rootView, id);
      if (tvContent == null) {
        break missingId;
      }

      return new ActivityMainBinding((RelativeLayout) rootView, tvContent);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

可以看到,显示通过 inflate 方法得到了 View 对象,然后通过 bind 方法中通过 findViewById 完成了控件的绑定。

而 getRoot 方法其实就是对根 View 的返回。

ViewBinding 封装

既然使用 ViewBinding 的套路是固定的,那么我们就可以将其抽取出来。

代码如下:

abstract class BaseActivity<T: ViewBinding>: AppCompatActivity() {

    lateinit var mViewBinding: T

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mViewBinding = getViewBinding()
        setContentView(mViewBinding.root)
    }

    abstract fun getViewBinding(): T

}

使用如下:

class SecondActivity: BaseActivity<ActivitySecondBinding>() {

    override fun getViewBinding(): ActivitySecondBinding {
        return ActivitySecondBinding.inflate(layoutInflater)
    }

    fun test(){
        mViewBinding.tv_content = "Sample"
    }

}