本章前言
这篇文章是在讲解kotlin协程
的时候扩展而来,如果对kotlin协程
感兴趣的可以通过下面链接进行阅读、
Kotlin
协程基础及原理系列
- 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
- 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
- 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
- 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
- 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装
Flow
系列
扩展系列
笔者也只是一个普普通通的开发者,设计不一定合理,大家可以自行吸收文章精华,去糟粕。
现在我们就可以开始做一些基础的封装工作,同时在app的bulid.gradle
文件中开启dataBinding
的使用
1 | kotlin复制代码android { |
基于DataBinding
的封装
我们先创建一个简单的布局文件activity_main.xml
。为了节约时间,同时我们也创建一个fragment_main.xml
保持一样的布局。
1 | xml复制代码<?xml version="1.0" encoding="utf-8"?> |
我们在使用DataBinding
初始化布局时候,我们通常喜欢使用下面几种方式,
在Activity
中:
1 | kotlin复制代码class MainActivity : AppCompatActivity() { |
在Fragment
中:
1 | kotlin复制代码class HomeFragment:Fragment() { |
这种情况下,每创建一个activity
和Fragment
都需要重写一遍。所以我们创建一个BaseActivity
进行抽象,然后使用泛型传入我们需要的ViewDataBinding
对象VB
,再通过构造方法或者抽象方法获取LayoutRes
资源
1 | kotlin复制代码abstract class BaseActivity<VB : ViewDataBinding>(@LayoutRes resId:Int) : AppCompatActivity() { |
这个时候是不是经过我们经过上面处理后,再使用的时候我们会方便很多。可能有些人封装过的人看到这里会想,你讲的这都是啥,这些我都会,没有一点创意。笔者想说:不要捉急,我们要讲的可不是上面的东西,毕竟做事情都需要前奏铺垫滴。
虽然经过上面的抽象以后,我们是减少了一些步骤。但是笔者还写觉得有些麻烦,因为我们还是需要手写的通过外部传一个LayoutRes
资源才能进行使用。想要再次细化缩减代码,那我们就得看看ActivityMainBinding
的实现。
我们在开启DataBinding
的时候,通过使用layout
的activity_main.xml
布局,DataBinding
在编译的时候会自动在我们的工程app/build/generated/data_binding_base_class_source_out/packname/databinding
目录下为我们生成一个ActivityMainBinding
类,我们看看它的实现:
1 | kotlin复制代码public abstract class ActivityMainBinding extends ViewDataBinding { |
我们可以看到在ActivityMainBinding
中的有4个inflate
方法,同时他们最后的都会直接使用我们的布局文件activity_main.xml
进行加载。所以我们想在上面的基础上进一步的简化使用方式,我们就必须通过反射的机制,从拿到ActivityMainBinding
中的inflate
方法,使用相对应的inflate
方法去加载我们的布局。代码如下:
1 | kotlin复制代码inline fun <VB:ViewBinding> Any.getViewBinding(inflater: LayoutInflater):VB{ |
我们先定义个扩展方法,通过反射的方式,从我们的Class
中拿到我们想要的泛型类ViewBinding
,然后invoke
调用ViewBinding
的inflate
方法。然后我们再创建一个接口用于BaseActivity
子类进行UI初始化绑定操作。
1 | kotlin复制代码interface BaseBinding<VB : ViewDataBinding> { |
1 | kotlin复制代码abstract class BaseActivity<VB : ViewDataBinding> : AppCompatActivity(), BaseBinding<VB> { |
现在我们就可以继承BaseActivity
实现我们的Activity
:
1 | kotlin复制代码class MainActivity : BaseActivity<ActivityMainBinding>() { |
1 | log复制代码D/MainActivity: btn :Hello World |
现在我们的代码是不是变得简洁、清爽很多。这样我们不仅节省了编写大量重复代码的时间,同时也让我们代码的变得更加合理、美观。
和Activity
有一些不同,因为Fragment
创建布局的时候需要传入ViewGroup
,所以我们稍微做一个变化。
1 | kotlin复制代码inline fun <VB:ViewBinding> Any.getViewBinding(inflater: LayoutInflater, container: ViewGroup?):VB{ |
可能在某些环境下有些人在创建Fragment
的时候需要把attachToRoot
设置成true
,这个时候自己扩展一个就好了,我们这里就不再演示。同理我们再抽象一个BaseFragment
:
1 | kotlin复制代码abstract class BaseFragment<VB : ViewDataBinding>: Fragment(),BaseBinding<VB> { |
1 | kotlin复制代码class HomeFragment:BaseFragment<FragmentMainBinding>() { |
看到这里是不是心灵舒畅了很多,不仅代码量减少了,逼格也提升了许多,同时又增加了XX技术群
里摸鱼吹水的时间。
当然我们也不能仅仅满足于此,在码代码的过程中还有一个大量重复工作的就是我们的Adapter
,我们就拿使用到做多的RecyclerView.Adapter
为例,假设我们创建一个最简单的HomeAdapter
:
1 | kotlin复制代码class HomeAdapter(private val data: List<String>? = null) : RecyclerView.Adapter<HomeAdapter.BindingViewHolder>() { |
就这样一个最简单的Adapter
,我们都需要些一堆啰嗦代码,如果再加上item
的click
事件的话,我们要做的
工作就变得更多。那么我们现在要解决这么一个问题的话,我们要处理哪里东西呢:
- 统一
Adapter
的初始化工作。 - 简化
onBindViewHolder
的使用。 - 去掉每次都需要重复创建
ViewHolder
。 - 统一我们设置
Item
的监听事件方式。 - 统一
Adapter
的数据刷新。
首先我们需要修改一下我们之前定义的扩展getViewBinding
,因为我们是不知道具体这个getViewBinding
是用在哪个类上,这个类又定义了几个泛型。所以我们增加一个默认值为0
的position
参数代替之前写死的0
,通过这种方式让调用者自行设定VB:ViewBinding
所在的位置顺序:
1 | kotlin复制代码inline fun <VB:ViewBinding> Any.getViewBinding(inflater: LayoutInflater,position:Int = 0):VB{ |
创建我们的BaseAdapter
,先将完整代码贴出:
1 | kotlin复制代码abstract class BaseAdapter<T, VB : ViewDataBinding> : RecyclerView.Adapter<BaseAdapter.BindViewHolder<VB>>() { |
我们这里先忽略这个BaseAdapter
的定义,现在我们将HomeAdapter
修改一下就变成了:
1 | kotlin复制代码class HomeAdapter : BaseAdapter<String, ItemHomeBinding>() { |
我们在Activity
中使用时:
1 | kotlin复制代码class MainActivity : BaseActivity<ActivityMainBinding>() { |
现在我们的adapter
中的代码是不是变的超简洁,我相信现在即使让你再写100
个Adapter
也不害怕。
现在我们来一步一步的拆解BaseAdapter
。我们看到BaseAdapter
需要传入2个泛型,T
是我们需要显示的实体的数据类型,VB
是我们的布局绑定ViewDataBinding
。
1 | KOTLIN复制代码abstract class BaseAdapter<T, VB : ViewDataBinding> |
往下可以看到我们通过在BaseAdapter
实现onCreateViewHolder
来处理Item
布局的初始化工作,我们这里调用getViewBinding
的时候position
传入的是1
,正好对应我们VB
所在的顺序:
1 | KOTLIN复制代码 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindViewHolder<VB> { |
同时我们创建了一个内部类BindViewHolder
来进行统一ViewHolder
的创建工作。
1 | KOTLIN复制代码 class BindViewHolder<M : ViewDataBinding>(var binding: M) : |
我们在初始化布局的同时又调用了一个空实现的setListener
方法。为什么我们在这里采用open
而不是采用abstract
来定义。是因为我们不是每一个Adapter
都需要设置Item
的监听事件,因此我们把setListener
只是作为一个可选的项来处理。
1 | kotlin复制代码 open fun VB.setListener() {} |
初始化完成以后,我们需要进行布局绑定,但是因为不同的Adapter
的界面,需要处理的绑定是不一样的,所以我们在实现onBindViewHolder
的同时,通过调用内部创建的抽象方法VB.onBindViewHolder
将我们的绑定处理交由子类进行处理。
1 | KOTLIN复制代码 override fun onBindViewHolder(holder: BindViewHolder<VB>, position: Int) { |
同时将VB.onBindViewHolder
参数转换为实际的数据bean
和对应的位置position
.
1 | KOTLIN复制代码 abstract fun VB.onBindViewHolder(bean: T, position: Int) |
到目前为止,我们在BaseAdapter
中已经处理了:
- 统一
Adapter
的初始化工作。 - 简化
onBindViewHolder
的使用。 - 去掉每次都需要重复创建
ViewHolder
。 - 统一我们设置
Item
的监听事件方式。
现在我们就来看下是如何统一Adapter
的数据刷新。可以看到我们在BaseAdapter
创建了一个私有数据集合mData
,在mData
中存放的是我们需要显示的泛型T
的数据类型。
1 | KOTLIN复制代码private var mData: List<T> = mutableListOf() |
同时我们增加了一个setData
方法,在此方法中我们使用DiffUtil
对我们的数据进行对比刷新。,如果对DiffUtil
不太熟的可以查一下它的方法。
1 | KOTLIN复制代码 fun setData(data: List<T>?) { |
这里我们需要注意一下,DiffUtil.Callback
中的areItemsTheSame
和areContentsTheSame
2个对比数据的方法,实际上是通过我们在BaseAdapter
中定义2个open
方法areItemContentsTheSame
,areItemsTheSame
。
1 | KOTLIN复制代码 protected open fun areItemContentsTheSame(oldItem: T, newItem: T, oldItemPosition: Int, newItemPosition: Int): Boolean { |
为什么要这么去定义的呢。虽然在BaseAdapter
中实现了这2个方法,因为我们不知道子类在实现的时候是否需要改变对比的方式。比如我在使用areItemsTheSame
的时候,泛型T
如果泛型T不是一个基本数据类型,通常只需要对比泛型T
中的唯一key
就可以。现在假设泛型T
是一个数据实体类User
:
1 | KOTLIN复制代码 data class User(val id:Int,val name:String) |
那我们在子类复写areItemsTheSame
方法的时候,就可以在我们的实现的apapter
如下使用:
1 | KOTLIN复制代码 protected open fun areItemsTheSame(oldItem: User, newItem: User): Boolean { |
细心的可能注意到我们有定义一个getActualPosition
方法,为什么不是叫getPosition
。这是因为在有些为了方便,我们在onBindViewHolder
的时候把此时position
保存下来,或者设置监听器的时候传入了position
。
如果我们在之前的数据基础上插入或者减少几条数据的话,但是又因为我们使用了DiffUtil
的方式去刷新,由于之前已存在bean
的数据没变,只是位置变了,所以onBindViewHolder
不会执行,这个时候我们直接使用position
的时候会出现位置不对问题,或者是越界的问题。比如如下使用:
1 | KOTLIN复制代码interface ItemClickListener<T> { |
我们在ItemClickListener
定义了2个方法,我们使用带有position
的onItemClick
方法来演示:
1 | XML复制代码<?xml version="1.0" encoding="utf-8"?> |
我们在XML中进行Click
绑定,然后我们在HomeAdapter
进行监听事件和数据设置
1 | KOTLIN复制代码class HomeAdapter(private val listener: ItemClickListener<String>) : BaseAdapter<String, ItemHomeBinding>() { |
接下来我们在MainActivity
通过2次设置数据在点击查看日志
1 | KOTLIN复制代码class MainActivity : BaseActivity<ActivityMainBinding>() { |
1 | logcat复制代码D/onItemClick: data:a position:0 |
所以我们需要在使用position
的时候,最好是通过getActualPosition
来获取真实的位置,我们修改一下itemClickListener
中的日志输出。
1 | KOTLIN复制代码 private val itemClickListener = object : ItemClickListener<String> { |
这个时候我们再重复上面操作的时候,就可以看到position
的位置就是它目前所处的真实位置。
1 | KOTLIN复制代码D/onItemClick: data:c position:0 |
到此为止,我们对于这个BaseAdapter<T, VB : ViewDataBinding>
的抽象原理,以及使用方式有了大概的了解。
需要注意的是:为了方便简单演示,我们这里假设是,没有在xml中直接使用Databinding
进行绑定。因为有些复杂逻辑我们是没有办法简单的在xml中进行绑定的。
很显然我们的工作并没有到此结束,因为我们的adapter
在常用的场景中还有多布局的情况,那我们又应该如何处理呢。
这个其实很好办。因为我们是多布局,那么就意味着我们需要把onCreateViewHolder
中的一部分工作暴露给子类处理,所以我们需要在上面BaseAdapter
的基础上做一些修改。照例上代码:
1 | KOTLIN复制代码abstract class BaseMultiTypeAdapter<T> : RecyclerView.Adapter<BaseMultiTypeAdapter.MultiTypeViewHolder>() { |
通过上面的代码可以看到,我们没有在BaseMultiTypeAdapter
中定义泛型VB :ViewDataBinding
,因为我们是多布局,如果都写在类的定义中明显是不合适的,我们也不知道在具体实现需要有多少个布局。
所以我们onCreateViewHolder
初始化布局的时候调用了一个抽象的onCreateMultiViewHolder
方法,这个方法交由我们具体业务实现类去实现。同时我们对onBindViewHolder
进行修改,增加了一个holder
参数供外部使用。
我们先数据实体类型
1 | KOTLIN复制代码sealed class Person(open val id :Int, open val name:String) |
和我们需要实现的Adapter业务类,:
1 | KOTLIN复制代码class SecondAdapter: BaseMultiTypeAdapter<Person>() { |
1 | KOTLIN复制代码class MainActivity : BaseActivity<ActivityMainBinding>() { |
运行一下就可以看到我们想要的结果:
1 | KOTLIN复制代码D/ItemTeacherBinding: item : Teacher(id=1, name=Person, subject=语文) position : 0 |
经过上面的处理以后,我们在创建Activiy
、Fragment
、Adapter
的时候减少了大量的代码。同时也节省了码这些重复垃圾代码的时间,起码让你们的工作效率起码提升10
个百分点,是不是感觉到自己无形中又变帅了许多。
经过以上封装处理以后,我们是不是也可以对Dialog
,PopWindow
、动态初始化View
进行处理呢。那还等什么,赶紧去实现吧。毕竟授人以鱼,不如授人以渔。
需要源码的看这里:demo源码
原创不易。如果您喜欢这篇文章,您可以动动小手点赞收藏。
Android技术交流群,有兴趣的可以私聊加入。
关联文章
Kotlin
协程基础及原理系列
- 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
- 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
- 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
- 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
- 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装
Flow
系列
扩展系列
本文转载自: 掘金