Android ViewBinding

500 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第17天,点击查看活动详情

为获取布局中的元素,传统方法是使用 findViewById 方法。如果页面元素很多,会使用一排该方法来获取对应的 View,显然会有大量重复代码。

传统的 findViewById 实现

Kotlin

findViewById<TextView>(R.id.sample_text).apply {
    text = viewModel.userName
}

Java

TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());

简化 findViewById 方案

ButterKnife

Jake Wharton 开源了 ButterKnife 使用 @BindView 注解来完成视图的绑定

class ExampleActivity extends Activity {
  @BindView(R.id.title) TextView title;
  @BindView(R.id.subtitle) TextView subtitle;
  @BindView(R.id.footer) TextView footer;
  
  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}

除了@BindView,该开源库也提供了其他的注解,如@OnClick, @BindBool, @BindColor 等,快捷的使用方式,备受 Android 开发者的青睐。

但作者也声明了,该工具不再维护了,而是推荐 View Binding,已发布的版本仍然可以使用。

image-20210406170309045.png

Kotlin 插件

随着 kotlin 语言的发展,kotlin 提供了支持的视图绑定的插件 kotlin-android-extensions

应用插件

apply plugin: 'kotlin-android-extensions'

kotlin 中使用

// import kotlinx.android.synthetic.main.<布局>.* 
import kotlinx.android.synthetic.main.activity_test.*

class TestActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
    
        //引用布局后,直接调用 id 名称进行操作
        tv_text.setText("aaaaaaaaa")
        tv_text.setOnClickListener(View.OnClickListener {
            Toast.makeText(this,"aaaaaaaa",Toast.LENGTH_SHORT).show()
        })
    }
}

使用起来也很方便,但是这个插件只针对于 kotlin 语言,java 还是没有办法使用的,而且在 kotlin 1.4.20-M2 中JetBrains 废弃了 Kotlin Android Extensions 编译插件,后面也不会再支持,官方推荐是使用 View Binding。 Migrate from Kotlin synthetics to Jetpack view binding

所以来看下备受推荐的 View Binding 是如何简化 findViewById 的

ViewBinding

Android Studio 3.6 开始支持 View binding,它可以将 layout 生成对应的的 java 文件

启动 ViewBinding

项目工程模块的 build.gradle 中加入以下配置:

// Available in Android Gradle Plugin 3.6.0
android {
    viewBinding {
        enabled = true
    }
}

// Android Studio 4.0
android {
    buildFeatures {
        viewBinding = true
    }
}

Binding 文件的生成

启动了 ViewBinding 功能之后,Android Studio 会自动为我们所编写的每一个布局文件都生成一个对应的 Binding 类。

Binding 类的命名规则是将布局文件按驼峰方式重命名后,再加上 Binding 作为结尾。

比如: activity_main.xml 布局,与之对应的 Binding 类就是 ActivityMainBinding

如果有些布局不希望生成对应的Binding 类,可以在布局文件的根元素位置添加声明

<LinearLayout
    xmlns:tools="http://schemas.android.com/tools"
    ...
    tools:viewBindingIgnore="true">
    ...
</LinearLayout>

Activity

在 Activiy 中使用方法

class MainActivity : AppCompatActivity() {

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

首先我们要调用 activity_main.xml 布局文件对应的 Binding 类,也就是 ActivityMainBindinginflate() 函数去加载该布局,inflate() 函数接收一个 LayoutInflater 参数,在 Activity 中是可以直接获取到的。

调用 Binding类的 getRoot() 函数可以得到 activity_main.xml 中根元素的实例,调用 getTextView() 函数可以获得 id 为 textView 的元素实例。

Fragment

假设我们有一个布局文件叫 fragment_main.xml,那么启用 ViewBinding 功能之后会生成一个与其对应的FragmentMainBinding 类。

如果我们想要在 MainFragment 中去显示这个布局,就可以这样写:

class MainFragment : Fragment() {

    private var _binding: FragmentMainBinding? = null

    private val binding get() = _binding!!

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

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

首先最核心的逻辑仍然是调用 FragmentMainBindinginflate() 函数去加载 fragment_main.xml 布局文件,但由于这是在 Fragment 当中,所以使用了3个参数的 inflate() 函数重载,这和我们平时在Fragment中去加载布局文件的方式如出一辙。

不一样的地方在于,由于我们是在 onCreateView() 函数中加载的布局,那么理应在与其对应的 onDestroyView() 函数中对 binding 变量置空,从而保证 binding 变量的有效生命周期是在 onCreateView() 函数和 onDestroyView() 函数之间。

自定义View

自定义 View 使用方法大同小异

class MyView(context: Context) : ConstraintLayout(context) {

    private var mBinding: ItemMineBinding
    init {
        val view = LayoutInflater.from(context).inflate(R.layout.item_mine, this)
        mBinding = ItemMineBinding.bind(view)
    }
}

include 标签

如果在 activity_main.xml 中使用了 <include> 标签

<?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=".viewbinding.ViewBindingActivity">

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

</androidx.constraintlayout.widget.ConstraintLayout>

如果要引用 include 布局中的元素,可以使用 mBinding.includeTest.tvxxx

原理

View Binding 将模块中每个 xml 布局生成一个绑定对象

例如这个 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/textView"
        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>

View Binding 将生成 ActivityMainBinding.java

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

  @NonNull
  public final TextView textView;

View Binding 为每一个具有 id 的视图生成对应的属性,也会实现 ViewBinding 中的唯一方法 getRoot

ActivityMainBinding.java中,视图绑定会生成一个公共的inflate方法。

它调用bind,在那里它将获取布局并绑定属性,并进行一些错误检查。

bind方法中,生成的绑定对象将为每个要绑定的 View 调用findViewById

比较

image.png

参考

【译】迁移被废弃的Kotlin Android Extensions插件

Use view binding to replace findViewById

Android开发学习笔记——ViewBinding