ViewBinding封装之旅


/   今日科技快讯   /

昨日,微信青少年模式保护能力再升级,设置了该模式后,青少年只能在微信视频号观看平台精选的适合青少年的内容,目前该功能正逐步覆盖,用户更新到微信最新版本,并根据视频号弹窗消息指引或者在“我-设置-青少年模式”开启后体验。

/   作者简介   /

本篇文章来自DylanCai同学的投稿,和大家分享了他如何巧妙封装ViewBinding的思路,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

DylanCai的博客地址:

https://juejin.cn/user/4195392100243000

/   开始   /

之前作者写了篇文章关于《如何优雅地封装和使用 ViewBinding》,地址如下:

优雅地封装和使用 ViewBinding

文章主要讲使用 ViewBinding 的优势,可以减少空指针和类型转换异常,用于替代 findViewById、ButterKnife、Kotlin synthetic。讲了 ViewBinding 基础用法和封装建议,提供了两种封装思路,封装 Kotlin 拓展函数和封装到基类使用。

本文再来补充两个使用场景,怎么在 BaseRecyclerViewAdapterHelper (后面简称 BRVAH)使用 ViewBinding,和如何不用反射封装 ViewBinding。

/   BRVAH 如何封装 ViewBinding   /

先来讲下很多人关心的问题,BRVAH 怎么使用 ViewBinding。

使用拓展函数直接兼容

这是绝大多数人想不到的用法,可以直接兼容已实现 BRVAH 的 Adapter。先说一下个人之前用 Map 时偶然发现的一个 withDefault() 拓展函数,用法如下。

val map = hashMapOf<String, Int>().withDefault { -1 }
Log.d(TAG, map.getValue("test").toString())

调用 withDefault() 方法能给 Map 增加一个默认值,通过 getValue() 取值取不到时会返回前面设的默认值。

我当时看到的时候很惊讶,因为之前是尝试过给某个对象增加属性,但是试了很久无果。有些人会说 Kotlin 不是有拓展属性么,拓展是静态解析的,并不能在一个类中插入新成员。所以我赶紧看下源码是怎么实现的,发现还是有点巧妙的。

下面就带着大家用同样的思路来给 BaseViewHolder 增加一个 binding 属性。

首先肯定不能凭空增加一个属性,必须要有个类来持有,Map 的是写了个 MapWithDefault 保存默认值,所以我们写一个 BaseViewHolderWithBinding 装饰类来保存 binding 对象。

class BaseViewHolderWithBinding<VB : ViewBinding>(val binding: VB) : BaseViewHolder(binding.root)

再来看下 Map 的 withDefault() 做了什么,查看源码得知原来是一波偷天换日操作,把原来的 Map 换成 MapWithDefault,所以并不是有什么 Kotlin 的特殊语法,只是一种装饰模式的巧妙运用,利用拓展函数把装饰过程省了,使其看起来像是给一个类增加了属性。

同样的我们也写一个 withBinding() 函数,把原来的 BaseViewHolder 对象换成装饰后的 BaseViewHolderWithBinding 对象。

fun <VB : ViewBinding> BaseViewHolder.withBinding(bind: (View) -> VB): BaseViewHolder =
  BaseViewHolderWithBinding(bind(itemView))

还没完,前面的 Map 例子是改用了 getValue() 方法去取值,而不是原来的 get() 方法,因为需要判断一下是 MapWithDefault 才有办法拿到默认值,所以另写了一个取值方法。同样的我们还要再提供一个获取 binding 对象的方法,判断如果是 BaseViewHolderWithBinding 类,我们才获取 binding 对象。

@Suppress("UNCHECKED_CAST")
fun <VB : ViewBinding> BaseViewHolder.getViewBinding(): VB {
  if (this is BaseViewHolderWithBinding<*>) {
    return binding as VB
  } else {
    throw IllegalStateException("The binding could not be found.")
  }
}

这样我们就封装好了,要怎么用呢?重写创建默认 ViewHolder 的方法,调用 withBinding() 方法使其伴随一个 binding 对象,后面就可以通过 getViewBinding() 方法获取 binding 对象进行使用了。

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>()
      .apply {
        tvFoo.text = item.value
      }
  }
}

原理很简单,代码量也很少,但是思路很巧妙,用起来很舒服,很符合人们的直觉。不过这个封装思路也有弊端,并不是什么地方都能用。有以下要求:

  • 需要在创建对象的过程进行处理。像 Activity 的创建过程没法干涉就没法用在 Activity。

  • 需要能继承,因为装饰模式要求有共同的超基类。像 Retrofit 用了 final 进行修饰,没法继承也就没法用。

  • 装饰后不能影响装饰前的用法。因为我们是偷偷换的,要让用户无感知的,所以装饰的对象通常是不变的。像 Adapter 继承后可能会增加别的方法,是不应该用这个方式来用的。

虽然能给类增加属性很强大,但是局限性很大,有很多常见的场景都很难用上。如果大家想到哪些比较好的使用场景可以在评论区说一说。

封装基类

这是上篇文章封装 Adapter 的思路,不过需要做些修改,查看 BRVAH 源码可知设置空布局或头尾布局会用反射创建 ViewHolder,并且是反射参数是 View 类型的构造函数。所以我们要增加该构造函数,不然会有隐患。传 View 的话绑定类的类型就不确定了,所以把类的泛型去掉,用的时候再转类型。下面是 ViewHolder 的代码。

class BaseBindingHolder(private val binding: ViewBinding) : BaseViewHolder(binding.root) {
  constructor(itemView: View) : this(ViewBinding { itemView })

  @Suppress("UNCHECKED_CAST")
  fun <VB : ViewBinding> getViewBinding() = binding as VB
}

然后编写 Adapter 基类,把原来的 BaseViewHolder 替换了。用到了个人封装的库,代码量非常少。

abstract class BaseBindingQuickAdapter<T, VB : ViewBinding>(layoutResId: Int = -1) :
  BaseQuickAdapter<T, BaseBindingHolder<VB>>(layoutResId) {

  override fun onCreateDefViewHolder(parent: ViewGroup, viewType: Int) =
    BaseBindingHolder(inflateBindingWithGeneric<VB>(parent))
}

只需这么简单的封装,后面使用起来就非常舒服了。

class FooAdapter : BaseBindingQuickAdapter<Foo, ItemFooBinding>() {

  override fun convert(holder: BaseBindingHolder<ItemFooBinding>, item: Foo) {
    holder.getViewBinding<ItemFooBinding>().apply {
      tvFoo.text = item.value
    }
  }
}

这是个人更加推荐的用法,代码比较简洁。

/   不用反射封装 ViewBinding   /

这是给一些不愿意用反射的小伙伴进行补充的。用法会有点奇怪,个人还是更推荐用反射的方式来使用。

使用拓展函数和委托

前文的封装通过反射静态方法创建绑定对象,我们想去掉反射的话就需要用其它办法得到静态方法,那么怎么得到呢?

这就要用到 Kotlin 高阶函数和 lambda 表达式。

先讲一个 lambda 特性,能把方法作为参数。以点击事件作为例子,在设置监听器时,因为 OnClickListener 接口只有一个方法,所以可以用 Lambda 表达式进行简写,而 Lambda 支持把函数作为参数来传递,所以点击事件还能这么来写。

btnLogin.setOnClickListener(this::onLoginBtnClick)
private fun onLoginBtnClick(view: View){
  ...
}

简单来说就是点击事件只是需要一个方法进行事件回调,那么可以直接传一个方法。


上面的 this 代表的是 Activity 对象,所以 this::onLoginBtnClick 是指的是一个 Activity 的 onLoginBtnClick 方法。但是我们要调用的 inflate 方法是静态方法,对象都没创建呢,这该怎么写呢?我们可以换一个角度,想一下静态属性怎么写。其实静态属性相较于对象的属性是把对象改成类名来获取属性。同理,静态方法的 lambda 表达式也是把对象改成类名,所以我们要调用的 inflate 方法就可以写成 ActivityMainBinding::inflate。

只要把这个理解了,剩下的东西就简单了。我们基于前文的 Activity 封装代码进行修改,使用 Kotlin 高阶函数把 inflate() 方法作为参数传进来,这就可以移除反射的代码,直接调用 inflate 方法。下面是修改后的代码:

fun <VB : ViewBinding> Activity.binding(inflate: (LayoutInflater) -> VB) = lazy {
  inflate(layoutInflater).apply { setContentView(root) }
}

看不懂也没关系,知道怎么用就行,调用的时候要多传一个静态方法参数。

class MainActivity : AppCompatActivity() {
  private val binding by binding(ActivityMainBinding::inflate)
}

这样封装使用确实挺巧妙的,可以省去一次反射,并且代码量只是稍微多一点。但是大部分人看这段代码是看不懂的,会有很多疑问:这是在做什么的?怎么有个奇怪的参数?为什么这么用?为什么 Fragment 不是 inflate 而是 bind?代码阅读性会差一些。

补充这个用法更多的是想给大家拓展下封装思路,和给一些不愿意用反射的小伙伴多一种选择。个人更推荐前文反射的用法,用一点可以忽略的反射性能让代码能更好地阅读,并且与 ViewModel 的用法更加统一。

封装基类

同样的思路对还能对基类进行封装,把 inflate 方法通过构造函数传进去,在 onCreate 调用该方法创建 binding 对象。下面是封装的示例:

abstract class BaseBindingActivity<VB : ViewBinding>(
  private val inflate: (LayoutInflater) -> VB
) : AppCompatActivity() {
  lateinit var binding: VB
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = inflate(layoutInflater)
    setContentView(binding.root)
  }
}

使用时继承基类后在构造函数传入 inflate 静态方法,这就可以在类里获取 binding 对象了,不过代码会长不少。

class MainActivity : 
  BaseBindingActivity<ActivityMainBinding>(ActivityMainBinding::inflate) {
}

/   最终方案   /

最后推荐一下我个人封装的库 ViewBindingKTX ,项目地址为:

https://github.com/DylanCaiCoding/ViewBindingKTX

包含了上述的封装。最初写这个库主要是想让 Java 小伙伴更容易用上 ViewBinding,经过几个版本迭代后,现在是一个尽可能全面的 ViewBinding 工具,帮助大家在各种使用场景用尽可能少的代码来使用 ViewBinding,支持以下的场景和用法。

  • 支持 Kotlin 和 Java 用法;

  • 支持多种使用反射和不使用反射的用法;

  • 支持封装改造自己的基类,使其用上 ViewBinding;

  • 支持 BaseRecyclerViewAdapterHelper;

  • 支持 Activitiy、Fragment、Dialog、Adapter;

  • 支持实现自定义组合控件;

  • 支持 TabLayout 实现自定义标签布局;

  • 支持 DataBinding 自动设置 lifecycleOwner;

只需简单地添加配置和依赖即可快速使用。

allprojects {
    repositories {
        ...
        maven { url 'https://www.jitpack.io' }
    }
}
android {
    buildFeatures {
        viewBinding = true
    }
}

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

有多种用法可选,具体的请查看 WIKI 文档,这里分享一下个人比较喜欢的用法,能更加优雅地使用 Jetpack MVVM。

class MainActivity : AppCompatActivity() {

  private val binding: ActivityMainBinding by binding()
  private val viewModel: MainViewModel by viewModels()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding.vm = viewModel
  }
}

上面用到的是 DataBinding,有兴趣的自行去了解。

在文档的Q&A地址中,地址如下所示:

https://github.com/DylanCaiCoding/ViewBindingKTX/wiki/Q&A

有收录些常见的问题,比如 include 的布局怎么使用 ViewBinding?为什么 Fragment 要销毁绑定对象?DataBinding 和 ViewBinding 的关系等问题。本来是本文要讲的内容,但是加上后篇幅太长而且有点杂,就放到了文档里作为补充,大家可以去看一下。如果还有其它使用场景或者相关问题都可以提 isuss,我会继续完善库或者给大家答疑的。

/   总结   /

本文讲了 ViewBinding 在 BRVAH 的封装思路和不用反射的封装思路,讲了 Kotlin 结合装饰模式的封装技巧和高阶函数的使用技巧。后面介绍了个人封装的库 ViewBindingKTX,是目前最全面的 ViewBinding 库,让大家在各种场景用尽可能少的代码使用上 ViewBinding。如果你觉得有帮助的话,希望能点个 star 支持一下哟 ~ 我后面会分享更多封装相关的文章给大家。 

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

在Android手机上对https请求进行抓包

一起看 I/O | Android 更新一览

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值