ButterKnife VS ViewBinding,谁才是findView的未来?

最近Android Studio更新到了4.1版本,发现项目中使用ButterKnife注解id的代码出现了警告,警告信息如下:

Resource IDs will be non-final in Android Gradle Plugin version 5.0, avoid using them as annotation attributes

从警告信息中可以看到在Gradle 5.0的插件中Resource 的Id值将不会再是final类型,因此应该避免在注解属性中使用Id。这意味着当我们把Gradle插件升级到5.0版本之后ButterKnife将无法再被使用!同时,我们在ButterKnife的官方文档上也看到了ButterKnife被标注弃用的信息:

陪伴我们多年,曾经辉煌一时,不可一世的ButterKnife也要寿终正寝,即将迎来它生命的终点。借这个机会,我们不妨来回顾一下Android开发中findView的发展史,以及展望下findView的未来。

一、Android绑定View的发展史

从Android系统诞生至今,在代码中findView一直是Android开发者无法绕开的一道程序。从最初的findViewbyId到如今炙手可热的ViewBinding,期间涌现出了许多findView的方式,它们让findView变得更加简单,也让我们的代码变得更加简洁。但随着Android新技术的发展,这些findView的方法也正在被一个一个的抛弃。本节内容我们就来回顾一下Android开发中findView的发展史。

1.findViewById

findViewById是Android开发中最原始,也是最基础的一种获取View的方法。它由Google官方提供,在Android开发生态的早期也是唯一一种能够获取View的方式。虽然它使用简单且根正苗红,贯穿古今。但由于高度重复的代码结构深受开发者诟病。在一个复杂布局的页面仅仅是findViewById的代码往往就能达到数十行。开发者无时无刻不想着弃用这一方案,因此后续衍生出了多种获取View的方式来简化代码。但万变不离其宗,归根结底,这些方式最终都还是通过findViewById来实现的。虽然它是最不被开发者认可的一种的方式,但时至今日开发者也无法摆脱它笼罩着的阴影。一脸你看不惯我你打我呀的表情!

2.ButterKnife

就在大家都在唾弃findViewById的大量重复代码时,一个插件横空出世。它通过一个BindView注解,传入一个Resource Id就能轻松获取到Id对应的View。代码如下:

public class MainActivity extends AppCompatActivity {
  @BindView(R.id.text_view)
  TextView mTextView;

     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         ButterKnife.bind(this);
     }
    }

它就是红极一时,时至今日大家依然还在用着的ButterKnife。ButterKnife通过最前沿的Java技术(最初的版本可能是反射,未加考究)--Java编译时注解处理器,在编译时自动生成findViewById的代码。例如,上边的例子通过ButterKnife会生成一个MainActivity_ViewBinding 类,在这个类中通过findViewById为mTextView赋值,其代码如下:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.mTextView = Utils.findRequiredViewAsType(source, R.id.text_view, "field 'mTextView'", TextView.class);
  }

  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleare![在这里插入图片描述](https://img-blog.csdnimg.cn/20190825162306549.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70)d.");
    this.target = null;

    target.mTextView = null;
  }
}

这一操作省去了开发者手动编写findViewById的时间,大大简化了代码,同时提高了开发效率。在当时的开发者看来ButterKnife不得不说是一个神器,以至于到后来成了Android项目开发的标配。

后来,随着Android Studio的诞生,Eclipse开发Android项目逐渐淡出历史舞台。Android studio的出现,带来了全新的技术,模块化风靡一时。大概在这个时候,Google官方似乎就已经有了改造R类的想法。在Android项目的library模块中,生成R类中的成员变量就已经改为了非final修饰。同时,Google官方也不再建议在app模块的代码中使用像:switch(view.getId())这样的代码。

正如Android studio官网文档《Non-constant Fields in Case Labels》上给出的原因:

In other words, the constants are not final in a library project. The reason for this is simple: When multiple library projects are combined, the actual values of the fields (which must be unique) could collide. Before ADT 14, all fields were final, so as a result, all libraries had to have all their resources and associated Java code recompiled along with the main project whenever they were used. This was bad for performance, since it made builds very slow. It also prevented distributing library projects that didn't include the source code, limiting the usage scope of library projects.

这一改变直接致使ButterKnife无法在Android项目的library模块中使用。而此时,ButterKnife正是如日中天,追随的开发者不计其数。为了能够让ButterKnife运行在library模块,ButterKnife的作者Jake Wharton大佬曲线救国,通过生成R2类让ButterKnife在library模块中复活,并且得以发展壮大。但不得不说,此时的ButterKnife就已经埋下了深深的隐患,并导致了其最终的溃败。

3.DataBinding

DataBinding是Google官方在2015年谷歌I/O大会上发布的一个数据绑定框架,它并非专为findView而生,而是作为MVVM架构的双向绑定数据的工具。findView的功能仅仅是DataBinding的一个附赠品。

开发者一般会在MVVM架构的项目中使用DataBinding来获取View。但是它也有很多诟病,比如需要修改xml的结构,在xml外部嵌套一个标签。并且很多情况下需要手动build才能生成DataBinding相关类。诸如此类问题,自然不会得到开发者的青睐。

关于DataBinding的详细使用在这里不做探讨。

4.Kotlin Android Extensions

2017年Google I/O开发者大会中,Google宣布Kotlin成为Android开发的一级语言,自此,Kotlin “转正”与Java并驾齐驱。而JetBrain推出的Kotlin Android Extension(以下简称KAE)插件成为了有史以来最简单的获取View的方法,简单到无需任何代码,直接通过id作为View使用。这一功能足以让所有Android开发者抓狂,纷纷感叹这才是findView的未来啊,终于可以和裹挟开发者十多年的findViewById说拜拜了! 作为一个Android开发者,不知道你是否会好奇Kotlin是如何将Id作为View的?我们不妨写一个简单的例子:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView.text = "Test"
    }
}

布局文件中TextView的id设置为“textView”,则在Activity中可以直接将textView作为一个TextView来使用。我们通过Android Studio的工具将kotlin的字节码反编译成Java代码看下

通过上述操作,打开kotlin的字节码后,再通过Decompile反编译成Java代码,则会得到如下图所示的结果:

通过反编译得到的Java代码我们发现Kotlin的这一操作其实也是通过findViewById实现的。只是通过插件的方式让我们感觉上是用了View的Id。

通过Kotlin的扩展插件来find view,无疑是一种优秀的方案。但这一方案并不是无懈可击。它存在以下几个缺点:

  • 类型安全:res下的任何id都可以被访问,有可能因访问了非当前Layout下的id而出错

  • 空安全:这主要体现在Configuration中的对应布局不全时,运行时可能出现NPE

  • 兼容性:只能在kotlin中使用,java不友好

  • 局限性:不能跨module使用

也正是这几个缺点导致了KAE的大溃败。随着Google对亲儿子ViewBinding的大力推广,KAE最终也招架不住,只能缴械投降---Jetbrains在官网宣布废弃KAE,并推荐开发者使用ViewBinding.

5.ViewBinding

到这里,以上提到的多种findView方案都已经被废弃,唯独只剩Google官方正在大力推广的ViewBinding组件。ViewBinding是Google在2019年I/O大会上公布的一款Android视图绑定工具。它的使用方式有点类似DataBinding,但相比DataBinding,ViewBinding是一个更轻量级、更纯粹的findViewById的替代方案。它具有以下几个优点:

  • 类型安全: ViewBinding会基于布局中的View生成类型正确的属性。比如,在布局中放入了一个 TextView ,视图绑定就会暴露出一个 TextView 类型的属性供开发中使用。

  • 空安全:ViewBinding会检测某个视图是不是只在一些配置下存在,并依据结果生成带有 @Nullable 注解的属性。所以即使在多种配置下定义的布局文件,视图绑定依然能够保证空安全。

  • ViewBinding生成的绑定类是一个Java类,并且添加了Kotlin的注解,可以很好的支持 Java 和 Kotlin 两种编程语言。

同时,Google官方还给出了一个ViewBinding、ButterKnife以及KAE的对比,如下图:

总而言之,到目前为止除了ViewBinding我们已经别无选择。那么不妨接下来详细探究下ViewBinding的使用方法。

二、ViewBinding使用详解

1.开启ViewBinding

Android Studio对于ViewBinding的支持是从3.6版本开始的,AS 3.6版本内置了Gradle插件。只需要在build.gradle中通过以下配置即可开启ViewBinding:

android {
    buildFeatures {
        viewBinding = true
    }
}

如果,你的项目存在多个模块,则需要在每个模块的gradle中添加上述配置。完成以上配置后ViewBinding会为所有布局文件自动生成对应的绑定类。且无须修改原有布局的 XML 文件,ViewBinding会根据现有的布局自动完成所有工作。

2.在Activity中使用ViewBinding

首先编写activity_main.xml的布局文件,如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
image.gif

完成后gradle插件会自动生成一个名为ActivityMainBinding的Java类,在Activity中通过ActivityMainBinding获取Binding实例,如下:

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.textView.text = "Hello World"
    }
image.gif

3.ViewBinding与include标签

在项目开发中,通常我们会使用include标签来简化布局文件,那么在使用了include标签的布局文件中,应该如何使用ViewBinding呢?且看代码:

// activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <include
        android:id="@+id/include"
        layout="@layout/layout_include" />

</androidx.constraintlayout.widget.ConstraintLayout>

// layout_include.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
image.gif

上述两个布局文件会分别生成ActivityMainBinding与LayoutIncludeBinding两个Java类,并且ActivityMainBinding类中通过组合依赖了LayoutIncludeBinding类。因此,使用方式如下:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        //  从ActivityMainBinding中获取LayoutIncludeBinding
        val include = binding.include
        // 通过LayoutIncludeBinding为TextView赋值
        include.tvText.text = "Hello World"
    }

image.gif

如果layout_include.xml文件位于子模块,经实践与以上代码的使用方式并无任何差异,但一定要在子模块中开启ViewBinding才行。

4.ViewBinding在Fragment中的使用 在Fragment中使用ViewBinding与Activity中有些差异,这里为了简便,我们使用上述中的activity_main.xml作为Fragment的布局文件,则Fragment的代码如下:

    private lateinit var binding: ActivityMainBinding
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = ActivityMainBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.textView.text="Hello World"
    }
image.gif

5.ViewBinding在RecyclerView#Adapter中的使用

布局文件不再贴出,直接看Adapter的代码,如下所示:

 class TestAdapter : RecyclerView.Adapter<TestViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder {
        val binding =
            ItemTestBinding.inflate(LayoutInflater.from(parent.context))
        return TestViewHolder(binding)
    }

    override fun onBindViewHolder(holder: TestViewHolder, position: Int) {
        holder.binding.textView.text = "Hello World"
    }

    override fun getItemCount(): Int {
        return 10
    }

    class TestViewHolder(var binding: ItemTestBinding) :
        RecyclerView.ViewHolder(binding.root)
}

image.gif

通过以上几个实例可以看到ViewBinding的使用是非常简单的。而ViewBinding的实现原理也并不难,Gradle插件会根据布局文件在项目的build目录下生成相应的ViewBinding类,并且,最终也是通过findViewById来完成View的获取的。具体实现代码不再贴出,感兴趣的同学可以自行查看。

三、展望与总结

本篇文章详细介绍了Android开发中find view的发展史,以及当下正火的ViewBinding组件。时代在发展,Android获取View的方式仍在变化。ViewBinding是一个优秀的组件,但它真的是Android开发中获取View的最优方案吗?显然,并不是!因为ViewBinding归根结底还是通过findViewById实现,且需要插件生成相关的Binding类,虽然省去了手动编写,但是ViewBinding仍然没能解决代码冗余的问题。

那什么才是findViewById的未来呢?大概最好的find view就是没有find view吧!目前Google正在朝着这一方向努力,正在开发的Jetpack Compose库就是要取代Android的布局文件,彻底消除findViewById。相信在未来某一天,随着Jetpack Compose库的普及,这个旷日持久的findViewById之争也最终会画上一个句号。

推荐阅读更多精彩内容