优雅地封装和使用 ViewBinding,该替代 Kotlin synthetic 和 ButterKnife 了

优雅地封装和使用 ViewBinding,该替代 Kotlin synthetic 和 ButterKnife 了

Android小彩虹2021-07-14 15:27:26230A+A-

前言

上周看到官方公众号发文章说准备弃用 Kotlin Extensions Gradle 插件了。可能有些人不知道 Kotlin Extensions 插件是什么,就是用 Kotlin 写 Android 有个很爽的功能是,可以直接用布局里的 id 拿到控件对象。或许一些人经常这么写,但不知道是用一个插件实现的。要在 build.gradle 里配置了下面的代码才会生效,之前创建项目时会自动带上,现在最新版的模板已移除。

apply plugin: 'kotlin-android-extensions'

用 id 获取布局里的控件对象该插件的一个叫 Kotlin synthetic 的功能。貌似挺好的呀,不然写一个控件就要声明成 laterinit var 再调用 findViewById 才能拿到控件对象,写起来很繁琐。这么方便的功能官方为什么要弃用呢?详细内容的可以看这篇文章《Kotlin Android Extensions 的未来计划》,官方提到了以下几点:

  • 污染全局命名空间。
  • 不能暴露可空性信息。
  • 仅支持 Kotlin 代码。

官方的建议是用 ViewBinding 来代替 Kotlin synthetic 。那么相对的,ViewBinding 会有以下优势:

  • 不污染命名空间。这个在我放弃 Kotlin synthetics 用 ViewBinding 时很有感触,终于不用在类文件里看到小写下划线命名的对象了,终于都统一成驼峰命名,强迫症患者表示这波很舒服。
  • 可以减少获取控件的空指针异常。这是 Kotlin synthetics、ButterKnife、findViewById 都存在的问题,大家应该多多少少都有遇到过。而用 ViewBinding 的话,在布局上有什么控件才能获取什么控件,这就不会出错。
  • 支持 Java 代码。还在用 Java 的朋友可以考虑放弃 ButterKnife 了。
  • 还有一点官方没有提到,就是用了 ViewBinding 能够很方便地使用 DataBinding。假如现在还在用 MVP,在未来想用 Jetpack MVVM 时就很容易了。

不过我之前了解过 ViewBinding,使用起来还是有点繁琐,所以那时没有改用 ViewBinding。现在官方表示一年后要弃用 Kotlin synthetic,不能用 id 获取控件了,所以现在还是慢慢用起来吧。我花了些时间对 ViewBinding 进行封装后,觉得可以用来代替 Kotlin synthetic 或者 ButterKnife,用 Kotlin 或者 Java 的朋友赶紧来试试吧。

下面来讲一下 ViewBinding 怎么使用和个人的封装建议。

ViewBinding 的基础用法

首先要在 module 的 build.gradle 文件配置开启 ViewBinding:

android {
    ...
    viewBinding {
        enabled = true
    }
}

这样该模块下每个 XML 文件都生成一个对应的绑定类,每个绑定类会包含根视图以及具有 ID 的所有视图的引用。绑定类的命名是:将 XML 文件的名称转换为驼峰命名,并在末尾添加 “Binding” 。

比如现在有 activity_main.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" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">

  <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_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

这会生成一个叫 ActivityMainBinding 的绑定类。该类的对象可以通过 getRoot() 方法获得根布局,并且可以获得一个叫 tvHelloWorld 的 TextView 对象。

如果不想生成某个布局的绑定类,可以在根视图添加 tools:viewBindingIgnore="true" 属性。

那这个绑定类的对象怎么实例化呢?该类会生成相关的 inflate 静态方法,调用该方法即可获得绑定对象。

class MainActivity : AppCompatActivity() {

  private lateinit var binding: ActivityMainBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    binding.tvHelloWorld.text = "Hello Android!"
  }
}

在 Fragment 使用有点不同,由于 Fragment 的存在时间比其视图长,需要在 onDestroyView() 方法中清除对绑定类实例的所有引用,所以写起来会有点麻烦。

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

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

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding.tvHelloWorld.text = "Hello Android!"
  }

  override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
  }
}

还有在 Adapter 的使用,因为布局不是只创建一次,而是每有一项数据就会创建,不能像上面那样在 Adapter 里写一个 binding 全局变量,这样 binding 只会得到最后一次创建的视图。所以 binding 对象应该是给 ViewHolder 持有。

class TextAdapter(
  private val list: List<String>
) : RecyclerView.Adapter<TextAdapter.TextViewHolder>() {
  
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextViewHolder {
    val binding = ItemTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    return TextViewHolder(binding)
  }

  override fun onBindViewHolder(holder: TextViewHolder, position: Int) {
    val content = list[position]
    holder.binding.tvContent.text = content
  }

  override fun getItemCount() = list.size

  class TextViewHolder(val binding : ItemTextBinding) : RecyclerView.ViewHolder(binding.root)
}

常见的情况就讲完了,总结一下 ViewBinding 的用法是,获取绑定对象,然后用 getRoot() 方法拿到根视图来替代使用到布局的地方。后面就可以通过绑定对象获取布局上的控件对象。

一些使用 Java 的朋友可能会看不太懂上面的代码。这木有关系,因为不推荐直接用,模板代码用 Java 写起来更长。把上面文字看了,代码理解个大概,能比较清楚 ViewBinding 的用法就行了,接下来就是讲怎么封装来使用比较好。

ViewBinding 的封装建议

用惯了 Kotlin synthetic 用 id 获取控件,再看 ViewBinding 的用法多少会觉得有点繁琐,所以需要封装一下了,毕竟 ViewBinding 能减少 id 写错或类型写错导致的异常,而且前者快弃用了。个人想到了两种封装思路。

不依托于基类

类似在 Kotlin 使用 ViewModel 的用法,做到声明了对象即可使用,不用管是怎么创建的,不用考虑什么时候要清除实例,不用每次去写 inflate 的模板代码。这种用法的好处是想用就用,无需继承什么基类,泛用性更强,移植代码更加容易。会用到一些 Kotlin 的特性,不适用于 Java。Java 的推荐用法还在后面。

先来分析一下,首先肯定要调用 inflate() 方法,不然怎么实例化 binding 对象。但是我们可以做到使用前自动 inflate(),无需手动调用。这就用到延时委托来实现,在 Fragment 因为要清除实例后面另说。然后就是 inflate() 方法需要传 layoutInflater,而 Activity 、Dialog 都有提供对应 get 方法,所以就变成获取 Activity 、Dialog 对象,可以传参,但是更推荐写成拓展函数传进来。剩下一个问题,怎么调用 inflate() 方法,方法名和参数固定,可以用反射。但我们仍要一个 Class 对象,这可以通过内敛方法来获取泛型的 Class 对象。

上述的是封装思路,需要了解一些 Kotlin 的用法,有兴趣的自己去研究一下,涉及的知识点较多就不过多展开了。以下是封装好的代码:

inline fun <reified VB : ViewBinding> Activity.inflate() = lazy {
  inflateBinding<VB>(layoutInflater).apply { setContentView(root) }
}

inline fun <reified VB : ViewBinding> Dialog.inflate() = lazy {
  inflateBinding<VB>(layoutInflater).apply { setContentView(root) }
}

@Suppress("UNCHECKED_CAST")
inline fun <reified VB : ViewBinding> inflateBinding(layoutInflater: LayoutInflater) =
  VB::class.java.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as VB

看不懂的没关系,知道怎么用就行。下面是 Activity 的使用示例,省去了 inflate() 和 setContentView() 的代码,在 Dialog 使用是类似的。

class MainActivity : AppCompatActivity() {

  private val binding: ActivityMainBinding by inflate()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding.tvHelloWorld.text = "Hello Android!"
  }
}

而 Fragment 的封装就不一样了,首先 inflate() 方法还要传 parent 对象就不好处理,可以换个思路,我们用另一个生成的方法 bind(),只需传个 View,在 Fragment 很好拿。另外还需要释放 binding 对象,不能用延时委托改用属性委托。下面是封装的代码:

inline fun <reified VB : ViewBinding> Fragment.bindView() =
  FragmentBindingDelegate(VB::class.java)

class FragmentBindingDelegate<VB : ViewBinding>(
  private val clazz: Class<VB>
) : ReadOnlyProperty<Fragment, VB> {

  private var isInitialized = false
  private var _binding: VB? = null
  private val binding: VB get() = _binding!!

  override fun getValue(thisRef: Fragment, property: KProperty<*>): VB {
    if (!isInitialized) {
      thisRef.viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
        @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
        fun onDestroyView() {
          _binding = null
        }
      })
      _binding = clazz.getMethod("bind", View::class.java)
        .invoke(null, thisRef.requireView()) as VB
      isInitialized = true
    }
    return binding
  }
}

使用起来就体现出封装的优势了,不用特地写个 _binding 来清除实例对象,不用重写 onDestoryView() 方法。

class HomeFragment : Fragment(R.layout.fragment_home) {

  private val binding: FragmentHomeBinding by bindView()

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding.tvHelloWorld.text = "Hello Android!"
  }
}

构造函数里的布局记得别漏了,因为需要用布局创建出 View ,我们才能调用 bind() 方法。

还有列表的封装,前面说了 binding 对象是给 ViewHolder 持有,所以我们写一个 BindingViewHolder 来接收 binding。

class BindingViewHolder<VB : ViewBinding>(val binding: VB) : RecyclerView.ViewHolder(binding.root)

当然这还不够,因为需要个 binding 对象,同样要用到反射进行实例化。我们得到 binding 对象后可以顺便把 BindingViewHolder 对象创建了,所以直接封装一个创建的方法。

inline fun <reified T : ViewBinding> newBindingViewHolder(parent: ViewGroup): BindingViewHolder<T> {
  val method = T::class.java.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
  val binding = method.invoke(null, LayoutInflater.from(parent.context), parent, false) as T
  return BindingViewHolder(binding)
}

怎么用呢?在 onCreateViewHolder 调用封装的方法就创建了 BindingViewHolder 对象,然后在 onBindViewHolder 方法通过 holder 持有的 binding 就能拿到得到布局里控件了。

class TextAdapter(
  private val list: List<String>
) : RecyclerView.Adapter<BindingViewHolder<ItemTextBinding>>() {
  
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
    newBindingViewHolder<ItemTextBinding>(parent)

  override fun onBindViewHolder(holder: BindingViewHolder<ItemTextBinding>, position: Int) {
    val content = list[position]
    holder.binding.tvContent.text = content
  }

  override fun getItemCount() = list.size
}

以上的封装简化了绑定类固定的 inflate 模板代码和 Fragment 清除实例对象的代码,在普通的 Activity、Fragment、Dialog、Adapter 都能使用,非常灵活。接下来讲另外一种封装思路。

依托于基类

主要是把 binding 对象封装在基类里替换掉布局,这样可以进一步减少声明 binding 对象的代码。还有前面的用法在某些基类使用时可能会存在 setContentView() 的调用时机问题,因为用到 binding 才会实例化和设置根布局。也许还没设置根视图,基类就去找控件,遇到的话可以改用下面的方式封装。

因为这里想教大家怎么去改造自己的基类,会涉及到 Kotlin 和 Java 两种写法,还有几种类型的基类,讲完的话篇幅很长。所以写了一个库 ViewBindingKtx ,让大家用最少的代码使用上 ViewBinding,同时也方便自己平时在项目中使用。

下面只是介绍部分用法,完整的用法和例子请到 Github 中查看。如果觉得对你有帮助,希望能点个 star 支持一下。

在 build.gradle 里配置 viewBinding 和添加依赖。包含了前面封装的拓展函数,不想把代码拷来拷去的话也可以添加依赖来使用。

dependencies {
    implementation 'com.dylanc:viewbinding-ktx:1.0.0'
}

介绍一下如何改造 Java 写的 Activity 基类。首先要给基类增加一个继承 ViewBinding 的泛型,然后类里增加一个 binding 全局变量。用工具类初始化 binding,删掉原来设置布局的代码,改为设置 binding.getRoot()。以下是核心的代码。

public abstract class BaseBindingActivity<VB extends ViewBinding> extends AppCompatActivity {

  private VB binding;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = ViewBindingUtil.inflateWithGeneric(this, getLayoutInflater());
    setContentView(binding.getRoot());
  }

  public VB getBinding() {
    return binding;
  }
}

下面是基类改造后的使用示例。

class MainActivity extends BaseBindingActivity<ActivityMainBinding> {

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getBinding().tvHelloWorld.setText("Hello Android!");
  }
}

无需声明控件变量,代码简洁很多,而且不会有 id 写错或者类型转换的问题,所以赶紧把 ButterKnife 换了吧。

另外再讲一下列表的基类封装,这里以个人一直在使用的列表库 Drakeet/MultiType 为例子。先看下原本的用法,ViewDelegate 可以当成 Adapter 来看。

class FooViewDelegate : ItemViewDelegate<Foo, FooViewDelegate.ViewHolder>() {

  override fun onCreateViewHolder(context: Context, parent: ViewGroup): ViewHolder {
    return ViewHolder(
      LayoutInflater.from(context).inflate(R.layout.item_foo, parent, false)
    )
  }

  override fun onBindViewHolder(holder: ViewHolder, item: Foo) {
    holder.binding.tvFoo.text = item.value
  }

  class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val fooView: TextView = itemView.findViewById(R.id.foo)
  }
}

来封装基类,同样要在基类增加一个继承 ViewBinding 的泛型,然后将原来的 ViewHolder 换成 BindingViewHolder,最后在 onCreateViewHolder 方法里调用一个用泛型创建 BindingViewHolder 的方法。下面是封装好的代码。

abstract class BindingViewDelegate<T, VB : ViewBinding> : ItemViewDelegate<T, BindingViewHolder<VB>>() {

  override fun onCreateViewHolder(context: Context, parent: ViewGroup) =
    newBindingViewHolderWithGeneric<VB>(parent)
}

使用起来就简单很多,可以对比一下前面的基础用法。

class FooViewDelegate : BindingViewDelegate<Foo, ItemFooBinding>() {

  override fun onBindViewHolder(holder: BindingViewHolder<ItemFooBinding>, item: Foo) {
    holder.binding.tvFoo.text = item.value
  }
}

本文所封装的代码用到了反射,开启混淆时要增加以下配置:

-keepclassmembers class * implements androidx.viewbinding.ViewBinding {
  public static ** inflate(...);
  public static ** bind(***);
}

更多基类改造封装 ViewBinding 的 Java 、Kotlin 示例请到 GitHub 查看。

关于用反射进行封装

可能有些人比较介意用反射,其实我也不太想用,能有其它更好的方式实现谁会特意用反射呢。如果反射的使用带来了足够的便利性,个人觉得还是可以接受的。比如 ViewModel 的源码也用了反射进行实例化,相较于自己手动创建 ViewModel 对象,使用官方的 ViewModelProviders 获取 ViewModel 对象能在 Activity 和 Fragment 销毁重建时恢复数据。

其实本文的封装从本质上来说是和 ButterKnife 一样的。同样生成了绑定控件的类,ButterKnife 用注解生成,ViewBinding 解析 XML 生成。都用到了反射,调用 ButterKnife.bind(this) 时反射了一次,我们调用工具类方法时反射了一次。最终的目的都是减少模板代码的编写,让代码更简洁。所以用反射来封装 ViewBinding 个人觉得是合适的。

总结

本文讲了官方弃用 Kotlin Extensions 插件的原因和使用 ViewBinding 的好处,可以避免 id 写错或类型写错导致的异常。然后讲述了 ViewBinding 的基础用法,并给出了两种 ViewBinding 的封装建议。后面介绍了个人封装的库 ViewBindingKtx,让大家用最少的代码使用上 ViewBinding,所以该弃用 Kotlin synthetic 和 ButterKnife 了。如果你觉得有帮助,希望能点个 star 支持一下哟 ~ 我后面会分享更多封装相关的文章给大家。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们