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

这样该模块下每个 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”>

</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
) : 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 Activity.inflate() = lazy {
inflateBinding(layoutInflater).apply { setContentView(root) }
}

inline fun Dialog.inflate() = lazy {
inflateBinding(layoutInflater).apply { setContentView(root) }
}

@Suppress(“UNCHECKED_CAST”)
inline fun 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 对象,不能用延时委托改用属性委托。下面是封装的代码:(5 月 12 号更新增加 doOnDestroyView 方法)

inline fun Fragment.bindView() =
FragmentBindingDelegate(VB::class.java)

inline fun Fragment.doOnDestroyView(crossinline block: () -> Unit) =
viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroyView() {
block.invoke()
}
})

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

private var binding: VB? = null

@Suppress(“UNCHECKED_CAST”)
override fun getValue(thisRef: Fragment, property: KProperty<*>): VB {
if (binding == null) {
binding = clazz.getMethod(“bind”, View::class.java)
.invoke(null, thisRef.requireView()) as VB
thisRef.doOnDestroyView { binding = null }
}
return binding!!
}
}

使用起来就体现出封装的优势了,不用特地写个 _binding 对象和重写 onDestoryView() 方法来清除实例对象。另外,如果还有其它释放操作要在 binding 销毁前执行,需要写在 doOnDestroyView() 方法里。

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!”
doOnDestroyView {
// 在 binding 对象销毁前进行释放操作
}
}
}

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

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

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

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

inline fun newBindingViewHolder(parent: ViewGroup): BindingViewHolder {
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
) : RecyclerView.Adapter<BindingViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
newBindingViewHolder(parent)

override fun onBindViewHolder(holder: BindingViewHolder, 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()。以下是核心的代码。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

给大家分享一份移动架构大纲,包含了移动架构师需要掌握的所有的技术体系,大家可以对比一下自己不足或者欠缺的地方有方向的去学习提升;

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

tps://img2.imgtp.com/2024/03/13/H4lCoPEF.jpg" />

最后

给大家分享一份移动架构大纲,包含了移动架构师需要掌握的所有的技术体系,大家可以对比一下自己不足或者欠缺的地方有方向的去学习提升;

[外链图片转存中…(img-1cHfjsZi-1712370589590)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 28
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值