前言:由于 Java 虚拟机机制,Kotlin 能做到与 JAVA 100% 兼容。
JAVA 源代码编译生成 JAVA 字节码(.class文件)后再交给 JVM 执行。同理,Kotlin 同样是将源码编译为 .class 文件后交给 JVM 执行。

ViewBinding

https://developer.android.com/topic/libraries/view-binding

使用

开启 ViewBinding,在应用级 build.gradle中添加: viewBinding { enabled true }

为某个模块启用视图绑定功能后,系统会为该模块中包含的每个 XML 布局文件生成一个绑定类。每个绑定类均包含对根视图以及具有 ID 的所有视图的引用。系统会通过以下方式生成绑定类的名称:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加 “Binding” 一词。

例如 activity_main.xml 会生成一个名为 ActivityMainBinding 的类。

Activity 中使用视图绑定

如需设置绑定类的实例以供 Activity 使用,请在 Activity 的 onCreate() 方法中执行以下步骤:

  1. 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Activity 使用。
  2. 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
  3. 将根视图传递到 setContentView(),使其成为屏幕上的活动视图。
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    binding = ResultProfileBinding.inflate(layoutInflater)
    val view = binding.root
    setContentView(view)
}
fun XXX() {
    binding.button.setOnClickListener { ... }
    ...
}

Fragment 中使用视图绑定

如需设置绑定类的实例以供 Fragment 使用,请在 Fragment 的 onCreateView() 方法中执行以下步骤:

  1. 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Fragment 使用。
  2. 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
  3. onCreateView() 方法返回根视图,使其成为屏幕上的活动视图。
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

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

fun XXX() {
    binding.button.setOnClickListener { ... }
    ...
}

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

注意:Fragment 的存在时间比其视图长。请务必在 Fragment 的 onDestroyView() 方法中清除对绑定类实例的所有引用。

原理

在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。其实就是通过插件帮我们编写 findViewById 这类重复代码。


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

    @NonNull
    public final TextView tvTextView;

    private ActivityMainBinding(@NonNull ConstraintLayout rootView,
                                            @NonNull TextView rvDataList) {
        this.rootView = rootView;
        this.tvTextView = tvTextView;
    }

    @Override
    @NonNull
    public ConstraintLayout 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.
        String missingId;
        missingId:
        {
            TextView tvTextView = rootView.findViewById(R.id.tv_text);
            if (tvTextView == null) {
                missingId = "tvTextView";
                break missingId;
            }
            return new ActivityMainBinding((ConstraintLayout) rootView, tvTextView);
        }
        throw new NullPointerException("Missing required view with ID: ".concat(missingId));
    }
}

DataBinding

https://developer.android.com/topic/libraries/data-binding

使用

开启 DataBinding,在应用级 build.gradle中添加: dataBinding { enabled true }

DataBinding 一般与 viewModel 搭配使用

数据绑定库会自动生成将布局中的视图与您的数据对象绑定所需的类。该库提供了可在布局中使用的导入、变量和包含等功能。

该库的这些功能可与现有布局无缝地共存。例如,可以在表达式中使用的绑定变量在 data 元素(界面布局根元素的同级)内定义。这两个元素都封装在 layout 标记中,如以下示例所示:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewmodel"
            type="com.myapp.data.ViewModel" />
    </data>
    <ConstraintLayout... /> <!-- UI layout's root element -->
</layout>

绑定表达式

布局中的表达式使用 @{} 语法写入特性属性中。其中可以写入表达式语言如 + - * / % > < == 等运算符、按 ID 引用布局中的其他视图、事件处理函数如 android:onClick="@{() -> variableName.xxx()}" 等等。

绑定适配器

对于名为 example 的特性,库自动尝试查找接受兼容类型作为参数的方法 setExample(arg)

android:text="@{user.name}" 表达式为例,库会查找接受 user.getName() 所返回类型的 setText(arg) 方法。如果 user.getName() 的返回类型为 String,则库会查找接受 String 参数的 setText() 方法。如果表达式返回的是 int,则库会搜索接受 int 参数的 setText() 方法。

一些属性需要自定义绑定逻辑。例如,android:paddingLeft 特性没有关联的 setter,而是提供了 setPadding(left, top, right, bottom) 方法。使用 BindingAdapter 注释的静态绑定适配器方法支持自定义特性 setter 的调用方式。

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}

生成绑定类

系统会为每个布局文件生成一个绑定类。所有生成的绑定类都是从 ViewDataBinding 类继承而来。默认情况下,类名称基于布局文件的名称,它会转换为 Pascal 大小写形式并在末尾添加 Binding 后缀。以上布局文件名为 activity_main.xml,因此生成的对应类为 ActivityMainBinding。此类包含从布局属性(例如,user 变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。

  • 针对布局中具有 ID 的视图在绑定类中创建不可变字段, 相当与为每个视图调用 findViewId() 方法。
  • 为变量(variable-name)生成 settergetter 方法。
  • ...

compare

与 findViewById 相比,视图绑定具有 Null 安全,类型安全的优点。视图绑定会创建对视图的直接引用,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用 @Nullable 标记。。每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。

视图绑定与数据绑定:视图绑定不需要处理注释,因此编译时间更短。视图绑定不需要特别标记的 XML 布局文件,因此在应用中采用速度更快。数据绑定支持布局变量或布局表达式。数据绑定支持双向绑定。