原文:Exploring View Binding in Depth — Using ViewBinding with < include>, < merge>, adapters, fragments, and activities
作者:Somesh Kumar
译者:Fly_with24
译者注:2020.02.27更新,探讨了 `ViewBinding` 的空安全问题,见文章末尾
谷歌在2019 I/O 大会中的 What’s New in Architecture Components 介绍了 view binding
在 What’s New in Architecture Components 中,有一个简短的关于view binding 的演讲,演讲中将 view binding 与现有解决方案进行了比较,并进一步讨论了为什么view binding 比 data binding
或 Kotlin 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为 tvVersionName
的 TextView
,因此在使用view binding 时,我们要做的就是获取绑定类的引用,例如:
val binding: ActivitySplashBinding = ActivitySplashBinding.inflate(layoutInflater)
在 setContentView()
方法中使用 getRoot()
,该方法将返回布局的根布局。可以从我们创建的绑定类对象访问视图,并且可以在创建对象后立即使用它,如下所示:
binding.tvVersionName.text = getString(R.string.version)
在这里,绑定类知道 tvVersionName
是TextView
,因此我们不必担心类型转换。
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 有些不同。 我们需要传递 LayoutInflator
,ViewGroup
和一个 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旋转屏幕等状态的空安全问题,译者进行了测试,步骤如下:
- 创建横竖屏两套布局文件,内容只有一个
TextView
,id 分别为hello1
hello2
- 分别使用
kotlin
和java
创建activity
,使用ViewBinding
将hello1
text
设置为 “你好” - 运行项目并进行横竖屏切换
结论: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