[译]深入研究ViewBinding 在 include, merge, adapter, fragment, activity 中使用

8,397 阅读6分钟

原文:Exploring View Binding in Depth — Using ViewBinding with < include>, < merge>, adapters, fragments, and activities
作者:Somesh Kumar
译者:Fly_with24


译者注:2020.02.27更新,探讨了 `ViewBinding` 的空安全问题,见文章末尾

Image Source: Google I/O 2019

谷歌在2019 I/O 大会中的 What’s New in Architecture Components 介绍了 view binding

What’s New in Architecture Components 中,有一个简短的关于view binding 的演讲,演讲中将 view binding 与现有解决方案进行了比较,并进一步讨论了为什么view binding 比 data bindingKotlin synthetics 等现有解决方案更好。

对我而言,Kotlin synthetics 运行良好,但是没有编译时的安全性,这意味着所有 ID 都位于全局命名空间中。因此,如果您使用的 ID 具有相同的名称,并且从错误的布局导入 ID, 由于ID不是当前布局的一部分,导致崩溃,除非您将应用程序运行到该布局,否则无法提前知道这一点。

这篇文章很好地概述了 Kotlin synthetics 的问题

The Argument Over Kotlin Synthetics

View Binding 将在 Android Studio 3.6 稳定版中提供(译者注:当前Android Studio稳定版版本为3.5.3),如果您想要使用它,您可以下载 Android Studio 3.6(2020.02.25更新) 或者 Android Studio 4.0 Canary

view binding 的主要优点是所有绑定类都是由Gradle插件生成的,因此它对构建时间没有影响,并且具有编译时安全性(我们将在示例中看到)。

首先,启用 view binding, 我们需要在 module 的build.gradle文件中添加以下内容:

// Android Studio 3.6
android {
    viewBinding {
        enabled = true
    }
}

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

注意:视图绑定是逐模块启用的,因此,如果您具有多模块项目设置,则需要在每个 build.gradle 文件中添加以上代码。

如果要在特定的布局禁用 view binding,则需要在布局文件的根视图中添加 tools:viewBindingIgnore = “true”

启用后,我们可以立即开始使用它,并且当您完成同步 build.gradle 文件时,默认情况下会生成所有绑定类。

它通过将XML布局文件名转换为驼峰式大小写并在其末尾添加 Binding 来生成绑定类。 例如,如果您的布局文件名为 activity_splash,则它将生成绑定类为 ActivitySplashBinding

如何使用它?

activity 中使用

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.tvVersionName.text = getString(R.string.version)
    }

我们有一个名为 activity_splash 的布局文件,里面有一个ID为 tvVersionNameTextView ,因此在使用view binding 时,我们要做的就是获取绑定类的引用,例如:

val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater) 

setContentView() 方法中使用 getRoot() ,该方法将返回布局的根布局。可以从我们创建的绑定类对象访问视图,并且可以在创建对象后立即使用它,如下所示:

binding.tvVersionName.text = getString(R.string.version)

在这里,绑定类知道 tvVersionNameTextView,因此我们不必担心类型转换。

fragment 中使用

class HomeFragment : Fragment() {
    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!

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

在 fragment 中,使用 view binding 有些不同。 我们需要传递 LayoutInflatorViewGroup和一个 attachToRoot 布尔变量,这些变量是通过覆盖 onCreateView 获得的。

我们可以通过调用 binding.root 返回 view。您还注意到,我们使用了两个不同的变量 binding_binding,并且 _binding 变量在 onDestroyView() 中设置为null。

这是因为该 fragment 的生命周期与 activity 的生命周期不同,并且该fragment 可以超出其视图的生命周期,因此如果不将其设置为null,则可能会发生内存泄漏。

另一个变量通过 !! 使一个变量为可空值而使另一个变量为非空值避免了空检查。 。

在 RecyclerView adapter 中使用


class PaymentAdapter(private val paymentList: List<PaymentBean>) : RecyclerView.Adapter<PaymentAdapter.PaymentHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentHolder {
        val itemBinding = RowPaymentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return PaymentHolder(itemBinding)
    }

    override fun onBindViewHolder(holder: PaymentHolder, position: Int) {
        val paymentBean: PaymentBean = paymentList[position]
        holder.bind(paymentBean)
    }

    override fun getItemCount(): Int = paymentList.size

    class PaymentHolder(private val itemBinding: RowPaymentBinding) : RecyclerView.ViewHolder(itemBinding.root) {
        fun bind(paymentBean: PaymentBean) {
            itemBinding.tvPaymentInvoiceNumber.text = paymentBean.invoiceNumber
            itemBinding.tvPaymentAmount.text = paymentBean.totalAmount
        }
    }
}

row_payment.xml 是我们用于 RecyclerView item 的布局文件,对应生成的绑定类 RowPaymentBinding

现在,我们所需要做的就是在onCreateViewHolder() 中调用 inflate() 方法生成 RowPaymentBinding 对象并传递到 PaymentHolder 主构造器中,并将 itemBinding.root 传递给 RecyclerView .ViewHolder() 构造函数。

处理<include>标签

view binding 可以与 <include> 标签一起使用。 布局中通常包含两种 <include> 标签,带或不带<merge> 标签。

  • <inlude> 不带 <merge>标签

我们需要为<include> 分配一个 ID,然后使用该 ID 来访问包含布局中的视图。让我们来看一个例子。

app_bar.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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="?actionBarSize"
        android:background="?colorPrimary"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    
</androidx.constraintlayout.widget.ConstraintLayout>

main_layout.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <include
        android:id="@+id/appbar"
        layout="@layout/app_bar"
        app:layout_constraintTop_toTopOf="parent" />
    
</androidx.constraintlayout.widget.ConstraintLayout>

在上面的代码中,我们在布局文件中包括了一个通用工具栏,<include> 有一个 android:id=“@+id/appbar” ID,我们将使用它从 app_bar.xml 中访问工具栏并将其设置为我们的 action bar

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: MainLayoutBinding = MainLayoutBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setSupportActionBar(binding.appbar.toolbar)
    }
  • <inlude><merge>标签

当在一个布局中包含另一个布局时,我们通常使用一个带有 <merge> 标记的布局,这有助于消除布局嵌套。

placeholder.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    
    <TextView
        android:id="@+id/tvPlaceholder"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    
</merge>

fragment_order.xml

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

    <include layout="@layout/placeholder" />

</androidx.constraintlayout.widget.ConstraintLayout>

如果我们尝试为该 <include> 提供ID,view binding 不会在绑定类中生成ID,因此我们无法像使用普通 include 那样访问视图。

在这种情况下,我们有 PlaceholderBinding,它是 placeholder.xml<merge> 布局文件)的自动生成的类。我们可以调用其bind()方法并传递包含它的布局的根视图。

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentOrderBinding.inflate(layoutInflater, container, false)
        placeholderBinding = PlaceholderBinding.bind(binding.root)
        placeholderBinding.tvPlaceholder.text = getString(R.string.please_wait)
        return binding.root
  }

然后,我们可以从我们的类(如 placeholderBinding.tvPlaceholder.text)访问 placeholder.xml 内部的视图。

感谢阅读。希望收到您的评论。

译者补充

在 fragment 中使用 view binding 比较麻烦,译者提供一个 BaseFragment 的封装供大家参考

abstract class BaseFragment<T : ViewBinding>(layoutId: Int) : Fragment(layoutId) {
    private var _binding: T? = null

    val binding get() = _binding!!

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = initBinding(view)
        init()
    }

    /**
     * 初始化 [_binding]
     */
    abstract fun initBinding(view: View): T

    abstract fun init()
    
    override fun onDestroyView() {
        _binding = null
        super.onDestroyView()
    }
}
class HomeFragment : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) {

    override fun initBinding(view: View): FragmentHomeBinding = FragmentHomeBinding.bind(view)

    override fun init() {
        binding.viewPager.adapter = SectionsPagerAdapter(this)
        TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
            tab.text = TAB_TITLES[position]
        }.attach()
    }
}

译者补充2(2020.02.27更新)关于ViewBinding的空安全

关于ViewBinding旋转屏幕等状态的空安全问题,译者进行了测试,步骤如下:

  1. 创建横竖屏两套布局文件,内容只有一个TextView,id 分别为 hello1 hello2
  2. 分别使用 kotlinjava 创建 activity ,使用 ViewBindinghello1 text 设置为 “你好”
  3. 运行项目并进行横竖屏切换

结论:kotlin 语言项目编译不通过,java 语言项目正常运行,旋转后空指针异常

分析:打开 ViewBinding 生成的 Binding

  /**
   * This binding is not available in all configurations.
   * <p>
   * Present:
   * <ul>
   *   <li>layout/</li>
   * </ul>
   *
   * Absent:
   * <ul>
   *   <li>layout-land/</li>
   * </ul>
   */
  @Nullable
  public final TextView hello1;

  /**
   * This binding is not available in all configurations.
   * <p>
   * Present:
   * <ul>
   *   <li>layout-land/</li>
   * </ul>
   *
   * Absent:
   * <ul>
   *   <li>layout/</li>
   * </ul>
   */
  @Nullable
  public final TextView hello2;

@Nullable 注解 解释了 kotlin 为什么编译不通过,而对于 java 该注解仅会 lint 提示开发者该位置可能出现空指针


关于我


我是 fly_with24