原文:Exploring View Binding in Depth — Using ViewBinding with < include>, < merge>, adapters, fragments, and activities
做者:Somesh Kumar
译者:Fly_with24php
谷歌在2019 I/O 大会中的 What’s New in Architecture Components 介绍了 view binding
android
在 What’s New in Architecture Components 中,有一个简短的关于view binding 的演讲,演讲中将 view binding 与现有解决方案进行了比较,并进一步讨论了为何view binding 比 data binding
或 Kotlin synthetics
等现有解决方案更好。git
对我而言,Kotlin synthetics
运行良好,可是没有编译时的安全性,这意味着全部 ID 都位于全局命名空间中。所以,若是您使用的 ID 具备相同的名称,而且从错误的布局导入 ID, 因为ID不是当前布局的一部分,致使崩溃,除非您将应用程序运行到该布局,不然没法提早知道这一点。github
这篇文章很好地概述了 Kotlin synthetics
的问题安全
The Argument Over Kotlin Syntheticsbash
View Binding
将在 Android Studio 3.6 稳定版中提供(译者注:当前Android Studio稳定版版本为3.5.3),若是您想要使用它,您能够下载 Android Studio 3.6 RC3 或者 Android Studio 4.0 Canary 9app
view binding 的主要优势是全部绑定类都是由Gradle插件生成的,所以它对构建时间没有影响,而且具备编译时安全性(咱们将在示例中看到)。ide
首先,启用 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
。
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
,所以咱们没必要担忧类型转换。
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,则可能会发生内存泄漏。
另外一个变量经过 !!
使一个变量为可空值而使另外一个变量为非空值避免了空检查。 。
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()
}
}
复制代码
我是 fly_with24