阅读 4869

封装DataBinding让你少写万行代码

banners_twitter.png

本章前言

这篇文章是在讲解kotlin协程的时候扩展而来,如果对kotlin协程感兴趣的可以通过下面链接进行阅读、

Kotlin协程基础及原理系列

Flow系列

扩展系列

笔者也只是一个普普通通的开发者,设计不一定合理,大家可以自行吸收文章精华,去糟粕。

现在我们就可以开始做一些基础的封装工作,同时在app的bulid.gradle文件中开启dataBinding的使用

android {
    buildFeatures {
        dataBinding  = true
    }
   //省略...
}
复制代码

基于DataBinding的封装

我们先创建一个简单的布局文件activity_main.xml。为了节约时间,同时我们也创建一个fragment_main.xml保持一样的布局。

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.MainActivity">

        <Button
            android:id="@+id/btn"
            android:layout_width="100dp"
            android:layout_height="50dp"
            android:gravity="center"
            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.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

我们在使用DataBinding初始化布局时候,我们通常喜欢使用下面几种方式, 在Activity中:

class MainActivity : AppCompatActivity() {
 private lateinit var mBinding:ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
         mBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
    }
}
复制代码

Fragment中:

class HomeFragment:Fragment() {

    private lateinit var mBinding:FragmentMainBinding
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mBinding = DataBindingUtil.inflate(layoutInflater, R.layout.fragment_main,container,false)
        return mBinding.root
    }
}
复制代码

这种情况下,每创建一个activityFragment都需要重写一遍。所以我们创建一个BaseActivity进行抽象,然后使用泛型传入我们需要的ViewDataBinding对象VB,再通过构造方法或者抽象方法获取LayoutRes资源

abstract class BaseActivity<VB : ViewDataBinding>(@LayoutRes resId:Int) : AppCompatActivity() {
    lateinit var mBinding:VB
    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
        mBinding = DataBindingUtil.setContentView<VB>(this,resId)
    }
    //...
}
 //或者
abstract class BaseActivity<VB : ViewDataBinding> : AppCompatActivity() {
    lateinit var mBinding:VB
    @LayoutRes abstract fun getLayoutId():Int
    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
        mBinding = DataBindingUtil.setContentView<VB>(this,getLayoutId())
    }
}
复制代码

这个时候是不是经过我们经过上面处理后,再使用的时候我们会方便很多。可能有些人封装过的人看到这里会想,你讲的这都是啥,这些我都会,没有一点创意。笔者想说:不要捉急,我们要讲的可不是上面的东西,毕竟做事情都需要前奏铺垫滴。

image.png

虽然经过上面的抽象以后,我们是减少了一些步骤。但是笔者还写觉得有些麻烦,因为我们还是需要手写的通过外部传一个LayoutRes资源才能进行使用。想要再次细化缩减代码,那我们就得看看ActivityMainBinding的实现。

我们在开启DataBinding的时候,通过使用layoutactivity_main.xml布局,DataBinding在编译的时候会自动在我们的工程app/build/generated/data_binding_base_class_source_out/packname/databinding目录下为我们生成一个ActivityMainBinding类,我们看看它的实现:

public abstract class ActivityMainBinding extends ViewDataBinding {
  @NonNull
   public final Button btn;

  protected ActivityMainBinding(Object _bindingComponent, View _root, int _localFieldCount,
      TextView tv) {
    super(_bindingComponent, _root, _localFieldCount);
    this.btn = btn;
    this.recyclerView = recyclerView;
  }

  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup root, boolean attachToRoot) {
    return inflate(inflater, root, attachToRoot, DataBindingUtil.getDefaultComponent());
  }

  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup root, boolean attachToRoot, @Nullable Object component) {
    return ViewDataBinding.<ActivityMainBinding>inflateInternal(inflater, R.layout.activity_main, root, attachToRoot, component);
  }

  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, DataBindingUtil.getDefaultComponent());
  }

  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable Object component) {
    return ViewDataBinding.<ActivityMainBinding>inflateInternal(inflater, R.layout.activity_main, null, false, component);
  }
    //省略...
}
复制代码

我们可以看到在ActivityMainBinding中的有4个inflate方法,同时他们最后的都会直接使用我们的布局文件activity_main.xml进行加载。所以我们想在上面的基础上进一步的简化使用方式,我们就必须通过反射的机制,从拿到ActivityMainBinding中的inflate方法,使用相对应的inflate方法去加载我们的布局。代码如下:

inline fun <VB:ViewBinding> Any.getViewBinding(inflater: LayoutInflater):VB{
    val vbClass =  (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.filterIsInstance<Class<VB>>()
    val inflate = vbClass[0].getDeclaredMethod("inflate", LayoutInflater::class.java)
    return  inflate.invoke(null, inflater) as VB
}
复制代码

我们先定义个扩展方法,通过反射的方式,从我们的Class中拿到我们想要的泛型类ViewBinding,然后invoke调用ViewBindinginflate方法。然后我们再创建一个接口用于BaseActivity子类进行UI初始化绑定操作。

interface BaseBinding<VB : ViewDataBinding> {
    fun VB.initBinding()
}
复制代码
abstract class BaseActivity<VB : ViewDataBinding> : AppCompatActivity(), BaseBinding<VB> {
 internal val mBinding: VB by lazy(mode = LazyThreadSafetyMode.NONE) {
       getViewBinding(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)
        mBinding.initBinding()
    }
}
复制代码

现在我们就可以继承BaseActivity实现我们的Activity

class MainActivity : BaseActivity<ActivityMainBinding>() {

    override fun ActivityMainBinding.initBinding() {
        Log.d("MainActivity","btn  :${btn.text}")
    }
}
复制代码
D/MainActivity: btn  :Hello World
复制代码

现在我们的代码是不是变得简洁、清爽很多。这样我们不仅节省了编写大量重复代码的时间,同时也让我们代码的变得更加合理、美观。

image.png

Activity有一些不同,因为Fragment创建布局的时候需要传入ViewGroup,所以我们稍微做一个变化。

inline fun <VB:ViewBinding> Any.getViewBinding(inflater: LayoutInflater, container: ViewGroup?):VB{
    val vbClass =  (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.filterIsInstance<Class<VB>>()
    val inflate = vbClass[0].getDeclaredMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
    return inflate.invoke(null, inflater, container, false) as VB
}
复制代码

可能在某些环境下有些人在创建Fragment的时候需要把attachToRoot设置成true,这个时候自己扩展一个就好了,我们这里就不再演示。同理我们再抽象一个BaseFragment

abstract class BaseFragment<VB : ViewDataBinding>: Fragment(),BaseBinding<VB> {
    protected lateinit var mBinding:VB
        private set
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mBinding = getViewBinding(inflater,container)
        return mBinding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        mBinding.initBinding()
    }
    
    override fun onDestroy() {
    super.onDestroy()
    //此处记得取消绑定,避免内存泄露
    if(::mBinding.isInitialized){ 
        mBinding.unbind()
    }
}
复制代码
class HomeFragment:BaseFragment<FragmentMainBinding>() {
    override fun FragmentMainBinding.initBinding() {
        Log.d("HomeFragment","btn  :${btn.text}")
    }
}
复制代码

看到这里是不是心灵舒畅了很多,不仅代码量减少了,逼格也提升了许多,同时又增加了XX技术群里摸鱼吹水的时间。

image.png

当然我们也不能仅仅满足于此,在码代码的过程中还有一个大量重复工作的就是我们的Adapter,我们就拿使用到做多的RecyclerView.Adapter为例,假设我们创建一个最简单的HomeAdapter

class HomeAdapter(private val data: List<String>? = null) : RecyclerView.Adapter<HomeAdapter.BindingViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
        val mBinding = DataBindingUtil.inflate<ItemHomeBinding>(LayoutInflater.from(parent.context), R.layout.item_home ,parent, false)
        return BindingViewHolder(mBinding)
    }

    override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
        holder.binding.tv.text = data?.get(position) ?: ""
        //其他绑定...
        holder.binding.executePendingBindings()
    }

    fun setData(){
        //刷新数据...
    }

    override fun getItemCount(): Int {
        return data?.size ?: 0
    }

    class BindingViewHolder constructor(val binding: ItemHomeBinding) : RecyclerView.ViewHolder(binding.root)
}
复制代码

就这样一个最简单的Adapter,我们都需要些一堆啰嗦代码,如果再加上itemclick事件的话,我们要做的 工作就变得更多。那么我们现在要解决这么一个问题的话,我们要处理哪里东西呢:

  1. 统一Adapter的初始化工作。
  2. 简化onBindViewHolder的使用。
  3. 去掉每次都需要重复创建ViewHolder
  4. 统一我们设置Item的监听事件方式。
  5. 统一Adapter的数据刷新。

首先我们需要修改一下我们之前定义的扩展getViewBinding,因为我们是不知道具体这个getViewBinding是用在哪个类上,这个类又定义了几个泛型。所以我们增加一个默认值为0position参数代替之前写死的0,通过这种方式让调用者自行设定VB:ViewBinding所在的位置顺序:

inline fun <VB:ViewBinding> Any.getViewBinding(inflater: LayoutInflater,position:Int = 0):VB{
    val vbClass =  (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.filterIsInstance<Class<VB>>()
    val inflate = vbClass[position].getDeclaredMethod("inflate", LayoutInflater::class.java)
    return  inflate.invoke(null, inflater) as VB
}

inline fun <VB:ViewBinding> Any.getViewBinding(inflater: LayoutInflater, container: ViewGroup?,position:Int = 0):VB{
    val vbClass =  (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.filterIsInstance<Class<VB>>()
    val inflate = vbClass[position].getDeclaredMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
    return inflate.invoke(null, inflater, container, false) as VB
}
复制代码

创建我们的BaseAdapter,先将完整代码贴出:

abstract class BaseAdapter<T, VB : ViewDataBinding> : RecyclerView.Adapter<BaseAdapter.BindViewHolder<VB>>() {

    private var mData: List<T> = mutableListOf()

    fun setData(data: List<T>?) {
        data?.let {
            val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
                override fun getOldListSize(): Int {
                    return mData.size
                }

                override fun getNewListSize(): Int {
                    return it.size
                }

                override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    val oldData: T = mData[oldItemPosition]
                    val newData: T = it[newItemPosition]
                    return this@BaseAdapter.areItemsTheSame(oldData, newData)
                }

                override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    val oldData: T = mData[oldItemPosition]
                    val newData: T = it[newItemPosition]
                    return this@BaseAdapter.areItemContentsTheSame(oldData, newData, oldItemPosition, newItemPosition)
                }
            })
            mData = data
            result.dispatchUpdatesTo(this)
        } ?: let {
            mData = mutableListOf()
            notifyItemRangeChanged(0, mData.size)
        }

    }

    fun addData(data: List<T>?, position: Int? = null) {
        if (!data.isNullOrEmpty()) {
            with(LinkedList(mData)){
                position?.let {
                    val startPosition = when {
                        it < 0 -> 0
                        it >= size -> size
                        else -> it
                    }
                    addAll(startPosition, data)
                }?: addAll(data)
                setData(this)
            }
        }
    }

    protected open fun areItemContentsTheSame(oldItem: T, newItem: T, oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldItem == newItem
    }

    protected open fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
        return oldItem == newItem
    }

    fun getData(): List<T> {
        return mData
    }

    fun getItem(position: Int): T {
        return mData[position]
    }

    fun getActualPosition(data: T): Int {
        return mData.indexOf(data)
    }

    override fun getItemCount(): Int {
        return mData.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindViewHolder<VB> {
        return with(getViewBinding<VB>(LayoutInflater.from(parent.context), parent,1)) {
            setListener()
            BindViewHolder(this)
        }
    }

    override fun onBindViewHolder(holder: BindViewHolder<VB>, position: Int) {
        with(holder.binding){
           onBindViewHolder(getItem(position), position)
           executePendingBindings()
        }
    }

    open fun VB.setListener() {}

    abstract fun VB.onBindViewHolder(bean: T, position: Int)

    class BindViewHolder<M : ViewDataBinding>(var binding: M) :
            RecyclerView.ViewHolder(binding.root)
}
复制代码

我们这里先忽略这个BaseAdapter的定义,现在我们将HomeAdapter修改一下就变成了:

class HomeAdapter : BaseAdapter<String, ItemHomeBinding>() {

    override fun ItemHomeBinding.onBindViewHolder(bean: String, position: Int) {
       tv.text = bean
    }
}
复制代码

我们在Activity中使用时:

class MainActivity : BaseActivity<ActivityMainBinding>() {
    lateinit var homeAdapter:HomeAdapter
    override fun ActivityMainBinding.initBinding() {
        homeAdapter = HomeAdapter()
        with(recyclerView){
            layoutManager = LinearLayoutManager(this@MainActivity).apply {
                orientation = RecyclerView.VERTICAL
            }
            adapter = homeAdapter
        }
        homeAdapter.setData(listOf("a","b","c","d","e","f"))
    }
}
复制代码

现在我们的adapter中的代码是不是变的超简洁,我相信现在即使让你再写100Adapter也不害怕。

image.png

现在我们来一步一步的拆解BaseAdapter。我们看到BaseAdapter需要传入2个泛型,T是我们需要显示的实体的数据类型,VB是我们的布局绑定ViewDataBinding

abstract class BaseAdapter<T, VB : ViewDataBinding> 
    : RecyclerView.Adapter<BaseAdapter.BindViewHolder<VB>>() {
    //...
}
复制代码

往下可以看到我们通过在BaseAdapter实现onCreateViewHolder来处理Item布局的初始化工作,我们这里调用getViewBinding的时候position传入的是1,正好对应我们VB所在的顺序:

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindViewHolder<VB> {
        return with(getViewBinding<VB>(LayoutInflater.from(parent.context), parent,1)) {
            setListener()
            BindViewHolder(this)
        }
    }
复制代码

同时我们创建了一个内部类BindViewHolder来进行统一ViewHolder的创建工作。

    class BindViewHolder<M : ViewDataBinding>(var binding: M) :
            RecyclerView.ViewHolder(binding.root)
复制代码

我们在初始化布局的同时又调用了一个空实现的setListener方法。为什么我们在这里采用open而不是采用abstract来定义。是因为我们不是每一个Adapter都需要设置Item的监听事件,因此我们把setListener只是作为一个可选的项来处理。

    open fun VB.setListener() {}
复制代码

image.png

初始化完成以后,我们需要进行布局绑定,但是因为不同的Adapter的界面,需要处理的绑定是不一样的,所以我们在实现onBindViewHolder的同时,通过调用内部创建的抽象方法VB.onBindViewHolder将我们的绑定处理交由子类进行处理。

    override fun onBindViewHolder(holder: BindViewHolder<VB>, position: Int) {
        with(holder.binding){
           onBindViewHolder(getItem(position), position)
           executePendingBindings()
        }
    }
复制代码

同时将VB.onBindViewHolder参数转换为实际的数据bean和对应的位置position.

 abstract fun VB.onBindViewHolder(bean: T, position: Int)
复制代码

到目前为止,我们在BaseAdapter中已经处理了:

  1. 统一Adapter的初始化工作。
  2. 简化onBindViewHolder的使用。
  3. 去掉每次都需要重复创建ViewHolder
  4. 统一我们设置Item的监听事件方式。

现在我们就来看下是如何统一Adapter的数据刷新。可以看到我们在BaseAdapter创建了一个私有数据集合mData,在mData中存放的是我们需要显示的泛型T的数据类型。

private var mData: List<T> = mutableListOf()
复制代码

同时我们增加了一个setData方法,在此方法中我们使用DiffUtil对我们的数据进行对比刷新。,如果对DiffUtil不太熟的可以查一下它的方法。

    fun setData(data: List<T>?) {
        data?.let {
            val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
                override fun getOldListSize(): Int {
                    return mData.size
                }

                override fun getNewListSize(): Int {
                    return it.size
                }

                override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    val oldData: T = mData[oldItemPosition]
                    val newData: T = it[newItemPosition]
                    return this@BaseAdapter.areItemsTheSame(oldData, newData)
                }

                override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    val oldData: T = mData[oldItemPosition]
                    val newData: T = it[newItemPosition]
                    return this@BaseAdapter.areItemContentsTheSame(oldData, newData, oldItemPosition, newItemPosition)
                }
            })
            mData = data
            result.dispatchUpdatesTo(this)
        } ?: let {
            mData = mutableListOf()
            notifyItemRangeChanged(0, mData.size)
        }
    }
复制代码

这里我们需要注意一下,DiffUtil.Callback中的areItemsTheSameareContentsTheSame2个对比数据的方法,实际上是通过我们在BaseAdapter中定义2个open方法areItemContentsTheSame,areItemsTheSame

    protected open fun areItemContentsTheSame(oldItem: T, newItem: T, oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldItem == newItem
    }

    protected open fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
        return oldItem == newItem
    }
复制代码

为什么要这么去定义的呢。虽然在BaseAdapter中实现了这2个方法,因为我们不知道子类在实现的时候是否需要改变对比的方式。比如我在使用areItemsTheSame的时候,泛型T如果泛型T不是一个基本数据类型,通常只需要对比泛型T中的唯一key就可以。现在假设泛型T是一个数据实体类User:

    data class User(val id:Int,val name:String)
复制代码

那我们在子类复写areItemsTheSame方法的时候,就可以在我们的实现的apapter如下使用:

    protected open fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        return oldItem.id == newItem.id
    }
复制代码

细心的可能注意到我们有定义一个getActualPosition方法,为什么不是叫getPosition。这是因为在有些为了方便,我们在onBindViewHolder的时候把此时position保存下来,或者设置监听器的时候传入了position

如果我们在之前的数据基础上插入或者减少几条数据的话,但是又因为我们使用了DiffUtil的方式去刷新,由于之前已存在bean的数据没变,只是位置变了,所以onBindViewHolder不会执行,这个时候我们直接使用position的时候会出现位置不对问题,或者是越界的问题。比如如下使用:

interface ItemClickListener<T> {
    fun onItemClick(view: View,position:Int, data: T){}
    fun onItemClick(view: View, data: T)
}
复制代码

我们在ItemClickListener定义了2个方法,我们使用带有positiononItemClick方法来演示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="bean"
            type="String" />

        <variable
            name="position"
            type="int" />

        <variable
            name="itemClickListener"
            type="com.carman.kotlin.coroutine.interf.ItemClickListener" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/tv"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:onClick="@{(v)->itemClickListener.onItemClick(v,position,bean)}"
            android:textColor="@color/black"
            android:textSize="18sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

我们在XML中进行Click绑定,然后我们在HomeAdapter进行监听事件和数据设置

class HomeAdapter(private val listener: ItemClickListener<String>) : BaseAdapter<String, ItemHomeBinding>() {

    override fun ItemHomeBinding.setListener() {
      itemClickListener = listener
    }

    override fun ItemHomeBinding.onBindViewHolder(bean: String, position: Int) {
        this.bean = bean
        this.position = position
        tv.text = bean
    }
}
复制代码

接下来我们在MainActivity通过2次设置数据在点击查看日志

class MainActivity : BaseActivity<ActivityMainBinding>() {
    lateinit var homeAdapter:HomeAdapter
    override fun ActivityMainBinding.initBinding() {
        homeAdapter = HomeAdapter(itemClickListener)
        with(recyclerView){
            layoutManager = LinearLayoutManager(this@MainActivity).apply {
                orientation = RecyclerView.VERTICAL
            }
            adapter = homeAdapter
        }
        homeAdapter.setData(listOf("a","b","c","d","e","f"))
        btn.setOnClickListener {
              Log.d("刷新", "第二次setData")
            homeAdapter.setData(listOf("c","d","e","f"))
        }
    }

    private val itemClickListener = object : ItemClickListener<String> {
        override fun onItemClick(view: View, position: Int, data: String) {
            Log.d("onItemClick", "data:$data   position:$position")
        }
    }
}
复制代码
D/onItemClick: data:a   position:0
D/onItemClick: data:b   position:1
D/onItemClick: data:c   position:2
D/onItemClick: data:d   position:3
D/onItemClick: data:e   position:4
D/onItemClick: data:f   position:5
D/刷新: 第二次setData
D/onItemClick: data:c   position:2
D/onItemClick: data:d   position:3
D/onItemClick: data:e   position:4
D/onItemClick: data:f   position:5
复制代码

所以我们需要在使用position的时候,最好是通过getActualPosition来获取真实的位置,我们修改一下itemClickListener中的日志输出。

    private val itemClickListener = object : ItemClickListener<String> {
        override fun onItemClick(view: View, position: Int, data: String) {
            Log.d("onItemClick", "data:$data   position:${homeAdapter.getActualPosition(data)}")
        }
    }
复制代码

这个时候我们再重复上面操作的时候,就可以看到position的位置就是它目前所处的真实位置。

D/onItemClick: data:c   position:0
D/onItemClick: data:d   position:1
D/onItemClick: data:e   position:2
D/onItemClick: data:f   position:3
复制代码

到此为止,我们对于这个BaseAdapter<T, VB : ViewDataBinding>的抽象原理,以及使用方式有了大概的了解。

需要注意的是为了方便简单演示,我们这里假设是,没有在xml中直接使用Databinding进行绑定。因为有些复杂逻辑我们是没有办法简单的在xml中进行绑定的。

image.png

很显然我们的工作并没有到此结束,因为我们的adapter在常用的场景中还有多布局的情况,那我们又应该如何处理呢。

这个其实很好办。因为我们是多布局,那么就意味着我们需要把onCreateViewHolder中的一部分工作暴露给子类处理,所以我们需要在上面BaseAdapter的基础上做一些修改。照例上代码:

abstract class BaseMultiTypeAdapter<T> : RecyclerView.Adapter<BaseMultiTypeAdapter.MultiTypeViewHolder>() {

    private var mData: List<T> = mutableListOf()

    fun setData(data: List<T>?) {
        data?.let {
            val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
                override fun getOldListSize(): Int {
                    return mData.size
                }

                override fun getNewListSize(): Int {
                    return it.size
                }

                override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    val oldData: T = mData[oldItemPosition]
                    val newData: T = it[newItemPosition]
                    return this@BaseMultiTypeAdapter.areItemsTheSame(oldData, newData)
                }

                override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    val oldData: T = mData[oldItemPosition]
                    val newData: T = it[newItemPosition]
                    return this@BaseMultiTypeAdapter.areItemContentsTheSame(oldData, newData, oldItemPosition, newItemPosition)
                }
            })
            mData = data
            result.dispatchUpdatesTo(this)
        } ?: let {
            mData = mutableListOf()
            notifyItemRangeChanged(0, mData.size)
        }

    }

    fun addData(data: List<T>?, position: Int? = null) {
        if (!data.isNullOrEmpty()) {
            with(LinkedList(mData)) {
                position?.let {
                    val startPosition = when {
                        it < 0 -> 0
                        it >= size -> size
                        else -> it
                    }
                    addAll(startPosition, data)
                } ?: addAll(data)
                setData(this)
            }
        }
    }

    protected open fun areItemContentsTheSame(oldItem: T, newItem: T, oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldItem == newItem
    }

    protected open fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
        return oldItem == newItem
    }

    fun getData(): List<T> {
        return mData
    }

    fun getItem(position: Int): T {
        return mData[position]
    }

    fun getActualPosition(data: T): Int {
        return mData.indexOf(data)
    }

    override fun getItemCount(): Int {
        return mData.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MultiTypeViewHolder {
        return MultiTypeViewHolder(onCreateMultiViewHolder(parent, viewType))
    }

    override fun onBindViewHolder(holder: MultiTypeViewHolder, position: Int) {
        holder.onBindViewHolder(holder, getItem(position), position)
        holder.binding.executePendingBindings()
    }

    abstract fun MultiTypeViewHolder.onBindViewHolder(holder: MultiTypeViewHolder, item: T, position: Int)

    abstract fun onCreateMultiViewHolder(parent: ViewGroup, viewType: Int): ViewDataBinding

    protected fun <VB :ViewDataBinding> loadLayout(vbClass: Class<VB>,parent: ViewGroup): VB {
        val inflate = vbClass.getDeclaredMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
        return inflate.invoke(null, LayoutInflater.from(parent.context), parent, false) as VB
    }

    class MultiTypeViewHolder(var binding: ViewDataBinding) :
            RecyclerView.ViewHolder(binding.root)
}
复制代码

通过上面的代码可以看到,我们没有在BaseMultiTypeAdapter中定义泛型VB :ViewDataBinding,因为我们是多布局,如果都写在类的定义中明显是不合适的,我们也不知道在具体实现需要有多少个布局。

所以我们onCreateViewHolder初始化布局的时候调用了一个抽象的onCreateMultiViewHolder方法,这个方法交由我们具体业务实现类去实现。同时我们对onBindViewHolder进行修改,增加了一个holder参数供外部使用。 我们先数据实体类型

sealed class Person(open val id :Int, open val name:String)

data class Student(
        override val id:Int,
        override val name:String,
        val grade:String):Person(id, name)

data class Teacher(
        override val id:Int,
        override val name:String,
        val subject:String):Person(id, name)
复制代码

和我们需要实现的Adapter业务类,:

class SecondAdapter: BaseMultiTypeAdapter<Person>() {

    companion object{
        private const val ITEM_DEFAULT_TYPE = 0
        private const val ITEM_STUDENT_TYPE = 1
        private const val ITEM_TEACHER_TYPE = 2
    }

    override fun getItemViewType(position: Int): Int {
        return when(getItem(position)){
            is Student -> ITEM_STUDENT_TYPE
            is Teacher -> ITEM_TEACHER_TYPE
            else -> ITEM_DEFAULT_TYPE
        }
    }

    override fun onCreateMultiViewHolder(parent: ViewGroup, viewType: Int): ViewDataBinding {
      return when(viewType){
          ITEM_STUDENT_TYPE -> loadLayout(ItemStudentBinding::class.java,parent)
          ITEM_TEACHER_TYPE ->  loadLayout(ItemTeacherBinding::class.java,parent)
           else ->  loadLayout(ItemPersionBinding::class.java,parent)
       }
    }

    override fun MultiTypeViewHolder.onBindViewHolder(holder: MultiTypeViewHolder, item: Person, position: Int) {
        when(holder.binding){
            is ItemStudentBinding ->{
                Log.d("ItemStudentBinding","item : $item   position : $position")
            }
            is ItemTeacherBinding ->{
                Log.d("ItemTeacherBinding","item : $item   position : $position")
            }
        }
    }
}
复制代码
class MainActivity : BaseActivity<ActivityMainBinding>() {
    override fun ActivityMainBinding.initBinding() {
        val secondAdapter = SecondAdapter()
                with(recyclerView){
            layoutManager = LinearLayoutManager(this@MainActivity).apply {
                orientation = RecyclerView.VERTICAL
            }
            adapter = secondAdapter
        }
        secondAdapter.setData(
                listOf(
                        Teacher(1,"Person","语文"),
                        Student(2,"Person","一年级"),
                        Teacher(3,"Person","数学"),
                ))
    }
复制代码

运行一下就可以看到我们想要的结果:

D/ItemTeacherBinding: item : Teacher(id=1, name=Person, subject=语文)   position : 0
D/ItemStudentBinding: item : Student(id=2, name=Person, grade=一年级)   position : 1
D/ItemTeacherBinding: item : Teacher(id=3, name=Person, subject=数学)   position : 2
复制代码

经过上面的处理以后,我们在创建ActiviyFragmentAdapter的时候减少了大量的代码。同时也节省了码这些重复垃圾代码的时间,起码让你们的工作效率起码提升10个百分点,是不是感觉到自己无形中又变帅了许多。

image.png

经过以上封装处理以后,我们是不是也可以对DialogPopWindow、动态初始化View进行处理呢。那还等什么,赶紧去实现吧。毕竟授人以鱼,不如授人以渔。

需要源码的看这里:demo源码

原创不易。如果您喜欢这篇文章,您可以动动小手点赞收藏image.png

关联文章

这篇文章是在讲解kotlin协程的时候扩展而来,如果对kotlin协程感兴趣的可以通过下面链接进行阅读

Kotlin协程基础及原理系列

Flow系列

扩展系列

文章分类
Android
文章标签