更多 ViewBinding 的封装思路,适配 BRVAH 竟如此简单

4,783 阅读16分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 1 天,点击查看活动详情

本文已授权[郭霖]公众号独家发布

前言

前段时间优化 ViewBinding 的工具类时,突然想到了一个新的封装思路,能更进一步简化 ViewBinding 的使用。个人目前在网上没看到有人这样来封装 ViewBinding,感觉还是有必要分享一下。

不过可能有人会问,都 2022 年了还学 ViewBinding ?虽然现在官方在推 Jetpack Compose,但是 Compose 不仅要学 Kotlin,还要学一套新的写 UI 方式,学习成本非常高。很多人有着 “Java 又不是不能用”、“xml 布局又不是不能用”的想法,并不那么愿意或者没那么多时间去学 Compose,Copmpose 普及还是任重而道远。那么在 Compose 普及之前,ViewBinding 是最好的选择,个人认为还是有必要学一学的。即使现在有使用 Compose,也不会是全部页面都用,还是有些会写 xml 布局的。

先说明一下并不是推倒现有的封装方案,而是对已有的方案进行补充和改进,能扩展更多的使用场景。还有这并不是什么特别难想的思路,可能在别的场景有见过类似的思路,但是没看到有人在 ViewBinding 这么用。

本文会侧重讲封装,对 ViewBinding 不了解的可以先看看个人之前讲 ViewBinding 的文章。

  1. 《优雅地封装和使用 ViewBinding,该替代 Kotlin synthetic 和 ButterKnife 了》
  2. 《 ViewBinding 巧妙的封装思路,还能这样适配 BRVAH 》

ViewBinding 的本质

先了解清楚 ViewBinding 的本质,我们才能更好地封装 ViewBinding。

简单过下 ViewBinding 用法,在 build.gradle 配置使用 ViewBinding。

android {
    buildFeatures {
        viewBinding true
    }
}

配置之后每个布局都会生成对应的 ViewBinding 类,比如我们创建一个 layout_test.xml

<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">

  <TextView
    android:id="@+id/tv_hello_world"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello world!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

这就会生成一个 LayoutTestBinding 类,有三种创建 Binding 对象的方式,可根据情况进行选择。通过 ViewBinding 对象可以获得布局上声明了 id 的控件。

val binding = LayoutTestBinding.inflate(layoutInflater)
// val binding = LayoutTestBinding.inflate(layoutInflater, parent, false)
// val binding = LayoutTestBinding.bind(view)
binding.tvHelloWorld.text = "Hello Android!"

有些人可能会好奇这个 Binding 类是什么东西,我们一起来看下源码:

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

  @NonNull
  public final TextView tvHelloWorld;

  private LayoutTestBinding(@NonNull ConstraintLayout rootView, @NonNull TextView tvHelloWorld) {
    this.rootView = rootView;
    this.tvHelloWorld = tvHelloWorld;
  }

  @Override
  @NonNull
  public ConstraintLayout getRoot() {
    return rootView;
  }
  
  // ...
}

从上面部分的代码可以看到 Binding 类生成了布局上所有加了 id 的控件和根视图控件,然后这些控件都是在私有的构造函数传入。

我们再来看下剩下的代码,有三个创建 Binding 对象的静态函数。

public final class LayoutTestBinding implements ViewBinding {
  
  // ...

  @NonNull
  public static LayoutTestBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static LayoutTestBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.layout_test, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static LayoutTestBinding 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.
    int id;
    missingId: {
      id = R.id.tv_hello_world;
      TextView tvHelloWorld = rootView.findViewById(id);
      if (tvHelloWorld == null) {
        break missingId;
      }

      return new LayoutTestBinding((ConstraintLayout) rootView, tvHelloWorld);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

虽然有三个静态函数可以创建 Binding 对象,但其实只有 bind() 函数会创建,另外两个 inflate() 函数最终都是调用了 bind() 函数。而 bind() 函数只是做了最简单的 findViewById() 操作,那么其实三个静态函数最终都会走了一遍 findViewById() 逻辑,找到布局上所有声明了 id 的控件并创建 Binding 对象。

那么生成的 Binding 类的本质只是一个编译器自动帮我们生成的 findViewById() 工具,并不是什么高大上的东西。

还有一个很多人会忽略的点,三个创建 Binding 类的静态函数都会调用 findViewById() 把所有控件找到,所以 ViewBinding 不应该频繁创建的,需要做缓存。对此不清楚的话可能会写出以下代码:

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
  val binding = ItemTextBinding.bind(holder.itemView)
}

上面的用法是不推荐的,onBindViewHolder() 会很频繁的回调,每回调一次就执行一堆 findViewById(),这么用肯定不好。ViewBinding 对象要缓存到 ViewHolder 中。

封装思路

为什么封装呢?我们使用 ViewBinding 会有很多模板代码,比如:

private lateinit var binding: ActivityMainBinding

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

每个类都写一遍还是有些繁琐的,有必要封装一下简化代码。

已有的方案

那么怎么封装呢?封装 ViewBinding 需要做以下两件事:

  1. 创建 ViewBinding 对象;
  2. 缓存 ViewBinding 对象;

创建 ViewBinding 对象只有两种方案,第一种是反射静态方法创建 ViewBinding 对象。第二种是不使用反射,使用高阶函数把静态函数作为参数传进来,那么执行高阶函数就会调用静态函数创建 ViewBinding 对象。

然后就是缓存方案,为什么要缓存在前面特意提了,通常的方案是使用 Kotlin 属性委托,在属性的委托类声明一个变量进行缓存。如果只是延时初始化,可以直接用延时委托 lazy {...} 的方式进行创建,有些人会自定义一个委托类,其实作用和官方的延时委托是一样的,再写一个类其实没必要。但是在 Fragment 使用不仅要延时初始化,还要在 onDestroyView() 销毁对象,就只能自定义一个属性的委托类。

上面讲的都是已有的封装方案,在常见的场景使用是没问题的。但是缓存的方案会有局限性,就是用到了属性委托,需要声明一个属性。在 ActivityFragmentDialog 等继承的类里能声明一个 binding 属性,但是没法给已有的控件声明 binding 属性,除非再写一个控件的继承类,这么用的话感觉有点蠢。

所以在一些用不了属性委托的场景,需要一种新的缓存方式。

新的思路

先说一下个人之前给自己埋的一个坑。之前给 TabLayout 封装了一个快速实现自定义标签布局的扩展方法,用法如下:

TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
  tab.setCustomView<LayoutBottomTabBinding> {
    tvTitle.setText(titleList[position])
    ivIcon.setImageResource(iconList[position])
    ivIcon.contentDescription = titleList[position]
  }
}.attach()

这里自定义布局只会在初始化时设置一次,不需要缓存 ViewBinding 对象,而且也不好缓存。但是之后有人问我怎么在 OnTabSelectedListener 拿到前面设置的 ViewBinding 对象,他要改变字体和图标大小。

这就比较尴尬了,因为官方的代码我们改不了,没法用属性委托给 TabLayout 声明属性。当时没想到好的办法来保存 ViewBinding 对象,就让他调用 bind() 方法获取。

tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
  override fun onTabSelected(tab: TabLayout.Tab) {
    val binding = tab.customView?.let { LayoutBottomTabBinding.bind(it) }
    // ...
  }

  override fun onTabUnselected(tab: TabLayout.Tab) {
  }

  override fun onTabReselected(tab: TabLayout.Tab) {
  }
})

这个监听事件不会像 onBindViewHolder() 回调那么频繁,而且一般布局就两三个控件。即使每次切换底部标签都会 findViewById() 也能勉强接受吧。

但是个人有些完美主义,没能提供一个好的解决方案挺难受的。前段时间有空回头来想一下有没什么好的办法解决。经过了一系列分析后发现只能存到 Tab 对象中,那就翻一下源码看下官方有没预留什么给我们保存东西,如果没有就比较难办了。没想到真找到了个 tag 对象能保存,并且没有地方调用过。等等,View 不是也有 tag 吗?

没错,这就是新的封装思路。通过 bind() 方法只需要一个 View 就能得到 ViewBinding 对象,而我们有 View 了,又可以把 ViewBinding 对象设置给 View 的 tag。思路比较简单,不过可能对 ViewBinding 理解较深会往缓存的方面想才能想到吧。

简而言之,有 View 了就能得到 ViewBinding 对象并存起来,实现真正意义上的绑定。

应用场景

任意的 ViewHolder 获取 binding 对象

BaseRecyclerViewAdapterHelper 为例,由于 BaseViewHolder 是库提供的,如果想增加 binding 属性,必须写个类继承 BaseViewHolder。在个人之前的一篇文章分享了装饰模式 + 扩展函数的封装思路,能把继承类给隐藏了,看起来像是直接“增加”了个属性。

class FooAdapter : BaseQuickAdapter<Foo, BaseViewHolder>(R.layout.item_foo) {

  override fun onCreateDefViewHolder(parent: ViewGroup, viewType: Int) : BaseViewHolder { 
    return super.onCreateDefViewHolder(parent, viewType).withBinding { ItemFooBinding.bind(it) }
  }
    
  override fun convert(holder: BaseViewHolder, item: Foo) {
    holder.getViewBinding<ItemFooBinding>().tvFoo.text = item.value
  }
}

这可以在不改动原有代码的情况下直接使用 ViewBinding。不过需要重写一个创建的函数,还不够完美。

我们换个封装思路,并不声明 binding 对象,而是通过 itemView 的 tag 获取和缓存 binding 对象,这就不需要继承类,可以直接用扩展函数实现:

@Suppress("UNCHECKED_CAST")
fun <VB : ViewBinding> BaseViewHolder.getBinding(bind: (View) -> VB): VB =
  itemView.getTag(Int.MIN_VALUE) as? VB ?: bind(itemView).also { itemView.setTag(Int.MIN_VALUE, it) }

先通过 tag 获取 ViewBinding 对象,如果获取不到就创建一次并设置到 tag 中。使用 tag 需要传一个整形作为 key 值,个人建议用一个负数。

这样就不需要重写创建的方法,能直接通过 ViewHolder 得到 binding 对象,用法更加简单。

class FooAdapter : BaseQuickAdapter<Foo, BaseViewHolder>(R.layout.item_foo) {
    
  override fun convert(holder: BaseViewHolder, item: Foo) {
    holder.getBinding(ItemFooBinding::bind).tvFoo.text = item.value
  }
}

ps: 由于 BaseRecyclerViewAdapterHelper 创建 ViewHolder 会反射一次,那么创建 ViewBinding 不建议再用反射了。

优化 Fragment 属性委托销毁的时机

在 Fragment 用属性委托封装会用到 Lifecycle 监听生命周期释放 binding 对象。而 Lifecycle 与 Fragment 的生命周期回调是下面的顺序执行:

Fragment onCreateView
Lifecycle onCreateView
Lifecycle onDestroyView
Fragment onDestroyView

我们通常会在 Fragment 的 onDestroyView() 执行释放操作。

class SomeFragment: Fragment(R.layout.fragment_some) {
  private val binding by binding(FragmentSomeBinding::bind)
  // ...

  override fun onDestroyView() {
    super.onDestroyView()
    binding.someView.release()
  }
}

上面的代码会报错,因为根据前面的执行顺序,Lifecycle 会先销毁 binding 对象,在 Fragment 的 onDestroyView() 获取 binding 会报空指针。所以个人之前设计的 Fragment 用法是需要在另一个接口方法执行释放操作。

class SomeFragment: Fragment(R.layout.fragment_some), BindingLifecycleOwner {
  private val binding by binding(FragmentSomeBinding::bind)
  // ...
  
  override fun onDestroyViewBinding() {
    binding.someView.release()
  }
}

功能是没问题了,但是使用成本又稍微高了一点点。而用新的思路可以完美地解决该问题,我们在 Fragment 能获得 View,用 View 来缓存 binding 对象就不需要在属性委托声明缓存变量,也就不需要用 Lifecycle 释放缓存变量了。

fun <VB : ViewBinding> Fragment.binding(bind: (View) -> VB) = FragmentBindingDelegate(bind)

class FragmentBindingDelegate<VB : ViewBinding>(private val bind: (View) -> VB) : ReadOnlyProperty<Fragment, VB> {
  @Suppress("UNCHECKED_CAST")
  override fun getValue(thisRef: Fragment, property: KProperty<*>): VB =
    requireNotNull(thisRef.view) { "The property of ${property.name} has been destroyed." }
      .let { getTag(Int.MIN_VALUE) as? VB ?: bind(this).also { setTag(Int.MIN_VALUE, it) } }
}

这样封装的话只要 View 还存在,ViewBinding 对象就也存在,在 Fragment 的 onDestroyView() 执行释放操作就没有问题了。当 View 销毁后,ViewBinding 也一起销毁。

更新动态添加的布局

适用于 TabLayout 更新自定义标签布局、NavigationView 更新头部布局等动态添加布局的场景。

比如封装一个 NavigationView 更新头部布局的扩展函数:

fun <VB : ViewBinding> NavigationView.updateHeaderView(bind: (View) -> VB, index: Int = 0, block: VB.() -> Unit) =
  getHeaderView(index)?.let { getTag(Int.MIN_VALUE) as? VB ?: bind(this).also { setTag(Int.MIN_VALUE, it) } }?.run(block)
navigationView.updateHeaderView(LayoutNavHeaderBinding::bind) {
  tvNickname.text = nickname
}

最终方案

最后分享一下个人封装的 ViewBinding 库 —— ViewBindingKTX。可能有些小伙伴已经在使用了,目前已经升级到了 2.0 版本,推荐升级一下。

新版有对源码进行优化,之前为了方便一些人拷贝源码使用,就把很多方法写在一个 kt 文件。随着适配的场景变多,代码会看得有点乱,所以升级 2.0 版本后按功能进行了拆分,方便一些感兴趣的小伙伴阅读源码。

Feature

  • 支持 Kotlin 和 Java 用法
  • 支持多种使用反射和不使用反射的用法
  • 支持封装改造自己的基类,使其用上 ViewBinding
  • 支持 BaseRecyclerViewAdapterHelper
  • 支持 Activity、Fragment、Dialog、Adapter
  • 支持在 Fragment 自动释放绑定类的实例对象
  • 支持实现自定义组合控件
  • 支持创建 PopupWindow
  • 支持 TabLayout 实现自定义标签布局
  • 支持 NavigationView 设置头部控件
  • 支持无缝切换 DataBinding

Gradle

在根目录的 build.gradle 添加:

allprojects {
    repositories {
        // ...
        maven { url 'https://www.jitpack.io' }
    }
}

添加配置和依赖:

android {
    buildFeatures {
        viewBinding true
    }
}

dependencies {
    // 以下都是可选,请查看文档根据需要进行添加
    implementation 'com.github.DylanCaiCoding.ViewBindingKTX:viewbinding-ktx:2.0.5'
    implementation 'com.github.DylanCaiCoding.ViewBindingKTX:viewbinding-nonreflection-ktx:2.0.5'
    implementation 'com.github.DylanCaiCoding.ViewBindingKTX:viewbinding-base:2.0.5'
    implementation 'com.github.DylanCaiCoding.ViewBindingKTX:viewbinding-brvah:2.0.5'
}

2.0 版本新增功能

下面主要介绍 2.0 版本新增的功能,其它常见使用场景的 Kotlin、Java 用法可自行看下 使用文档

更简单地适配 BRVAH

与老版本相比不需重写创建 BaseViewHolder 的方法,用法更加简单。

Kotlin 用法:

class FooAdapter : BaseQuickAdapter<Foo, BaseViewHolder>(R.layout.item_foo) {

  override fun convert(holder: BaseViewHolder, item: Foo) {
    holder.getBinding(ItemFooBinding::bind).tvFoo.text = item.value
  }
}

Java 用法:

public class FooAdapter extends BaseQuickAdapter<Foo, BaseViewHolder> {

  public FooAdapter() {
    super(R.layout.item_foo);
  }

  @Override
  protected void convert(@NotNull BaseViewHolder holder, Foo foo) {
    ItemFooBinding binding = BaseViewHolderUtil.getBinding(holder, ItemFooBinding::bind);
    binding.tvFoo.setText(foo.getValue());
  }
}

随心所欲地自定义 TabLayout

之前只是支持快速实现 TabLayout 的自定义布局,比如 TabLayout + ViewPager2 快速实现自定义底部导航栏:

TabLayoutMediator(tabLayout, viewPager2, false) { tab, position ->
  tab.setCustomView<LayoutBottomTabBinding> {
    tvTitle.text = getString(tabs[position].title)
    ivIcon.setImageResource(tabs[position].icon)
    tvTitle.contentDescription = getString(tabs[position].title)
  }
}.attach()

现在增加了 TabLayout.updateCustomTab<VB> {...} 方法可以更新自定义的布局,比如收到消息后在第二个标签显示小红点:

viewModel.unreadCount.observe(this) { count ->
  tabLayout.updateCustomTab<LayoutBottomTabBinding>(1) {
    ivUnreadState.isVisible = count > 0
  }
}

还能使用 TabLayout.doOnCustomTabSelected<VB> (...) 监听点击事件,比如点击了第二个标签后,更新未读数量隐藏小红点:

tabLayout.doOnCustomTabSelected<LayoutBottomTabBinding>(
  onTabSelected = { tab ->
    if (tab.position == 1) {
      viewModel.unreadCount.value = 0
    }
  })

想加未读数量或者切换动画都很简单,基本可以实现任意布局的底部导航栏了。

快速实现简单的列表

之前考虑到大家用的列表适配器各不相同,所以只对 ViewHolder 进行了封装,并没有提供适配器基类。但是没有适配器基类并不是很方便,所以在 viewbinding-base 依赖增加了快速实现简单列表的用法。

这是基于 RecyclerViewListAdapter 实现的(注意不是 ListViewListAdapter)。可能有的人没用过,简单介绍一下,ListAdapter 是官方结合 DiffUtil 封装的适配器,设置最新的数据会自动执行改变的动画,用起来方便很多。

ListAdapter 之前需要写一个类继承 DiffUtil.ItemCallback<T>,实现两个方法,一个比较是否是同一项,一个比较内容是否相同。个人习惯将该类写在对应的实体类中,方便在 ListAdapter 复用。

data class Message(
  val id: String,
  val content: String
) {
  class DiffCallback : DiffUtil.ItemCallback<Message>() {
    override fun areItemsTheSame(oldItem: Message, newItem: Message) = oldItem.id == newItem.id
    override fun areContentsTheSame(oldItem: Message, newItem: Message) = oldItem == newItem
  }
}

之后就可以写个类继承 ListAdapter,需要在构造函数传入对应的 DiffUtil.ItemCallback<T>,其它的和写一个 Adapter 差不多。

class MessageAdapter : ListAdapter<Message, MessageAdapter.ViewHolder>(Message.DiffCallback()) {

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
    ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false))

  override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.tvContent.text = getItem(position).content
  }

  class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
    val tvContent: TextView = root.findViewById(R.id.tv_content)
  }
}

之后发送列表数据就会自动执行更新动画,不需要调用插入、移除等方法。

adapter.submitList(newList)

本库用 ListAdapter 结合 ViewBinding 封装了一个 SimpleListAdapter<T, VB> 类简化用法。

class MessageAdapter : SimpleListAdapter<Message, ItemMessageBinding>(Message.DiffCallback()) {

  override fun onBindViewHolder(binding: ItemMessageBinding, item: Message, position: Int) {
    binding.tvContent.text = item.content
  }
}

如果适配器不需要复用,可以用 simpleListAdapter<T, VB>(callback) {...} 的委托方法创建适配器,不需要特地声明一个类。

private val adapter by simpleListAdapter<Message, ItemMessageBinding>(Message.DiffCallback()) { item ->
  tvContent.text = item.content
}

如果是 IntLongFloatDoubleBooleanString 的基础数据类型,个人已经写好了对应的 Callback 类,可使用 simpleXXXListAdapter<VB> {...} 的委托方法,也提供了对应的 SimpleXXXListAdapter<VB> 基类。

private val adapter by simpleStringListAdapter<ItemFooBinding> {
  textView.text = it
}

支持设置点击事件和长按事件:

adapter.doOnItemClick { item, position ->
  // 点击事件
}
adapter.doOnItemLongClick { item, position ->
  // 长按事件
}

基本能满足简单列表的需求了,复杂列表的话就需要大家另外实现了,其它适配器可以使用 ViewHolder.getBinding<VB>() 的扩展函数。

支持 PopupWindow

private val popupWindow by popupWindow<LayoutPopupBinding> {
// private val popupWindow by popupWindow(LayoutPopupBinding::inflate) {
  btnLike.setOnClickListener { ... }
}

有个小伙伴提到了就顺手封装一下,大家如果觉得还缺什么 ViewBinding 的使用场景都可以提 Issues,个人会尽量满足。

ViewBinding 与 DataBinding

最后聊点题外话,聊聊 ViewBinding 与 DataBinding 的关系。因为之前时不时看到有人问 ViewBinding 和 DataBinding 该用哪一个?或者说你都用 DataBinding 了,干嘛还用 ViewBinding。很多人觉得这是两个东西,这样的观点是错误的。

其实最早的时候只有 DataBinding,能获取布局上的控件,并且能实现数据绑定。但是使用起来麻烦,需要在布局上加 <layout/> 标签,加上很多布局绑定的代码,代码侵入性极强。一旦用了,这个布局就不好复用了,除非其它项目也使用 DataBinding。但 DataBinding 获取控件是挺方便的,所以官方将这部分功能抽取出来得到 ViewBinding,并且优化了生成类的规则,不用在布局加代码就会自动生成。

所以 ViewBinding 和 DataBinding 两者不仅是包含关系,又是相辅相成的。为此官方还统一了两者的获取方式,最早 DataBinding 是使用 DataBindingUtil.setContentView(...) 获取 binding 对象的,按这个思路 ViewBinding 应该是使用 ViewBindingUtil.setContentView(...),而官方却没有这么做,反而是给两者都增加了同样的 inflate()bind() 方法,统一了获取方式。

也就是说官方希望 ViewBinding 和 DataBinding 是混着用的。平时就用 ViewBinding,需要数据绑定再开启 DataBinding,然后在布局加 <layout/> 标签写数据绑定代码,原本的 ViewBinding 代码不需要改就转成使用 DataBinding。

这才是正确的使用方式,并不是两者之中做选择,而是默认使用 ViewBinding,需要数据绑定时才用 DataBinding。所以个人封装的属性委托用法是 by binding() 而不是 by viewBinding(),也有这方面的原因,怕别人以为之只能用于 ViewBinding。

总结

本文讲了 ViewBinding 的本质,其实就是一个 findViewById() 的工具。后面分享一个新的封装思路,能补充一些在控件上获取 binding 对象的场景,能很简单的在 BRVAH 使用 ViewBinding。还有讲解了 ViewBinding 和 DataBinding 的关系。

后面分享了个人封装的库 ViewBindingKTX,介绍了 2.0 版本的新特性,适配 BRVAH 非常简单,完善了 TabLayout 和列表的用法,用起来更加方便了。如果您觉得有帮助的话,希望能点个 star 支持一下哟 ~ 个人后面会分享更多封装相关的文章给大家。

关于我

一个兴趣使然的程序“工匠”  。有代码洁癖,喜欢封装,对封装有一定的个人见解,有不少个人原创的封装思路。GitHub 有分享一些帮助搭建开发框架的开源库,有任何使用上的问题或者需求都可以提 issues 或者加我微信直接反馈。

讲解封装思路的文章