MultiTypeAdapter实现RecyclerView的多类型页面(ViewModel、json数据源)

179 阅读4分钟

一、效果示意

下图GIF展示了两个比较简单的子item type:TextView文本 和 Button按钮

Screenshot_2023_0319_172427.gif

二、依赖

使用到四个依赖,分别是:

RecyclerView、multitype、gson及apache的网络库(用于将HTTP内容字符的字节数组转换为字符串)

// (1)RecyclerView的依赖
api 'androidx.recyclerview:recyclerview:1.0.0'
// (2)multitype的依赖
api 'com.drakeet.multitype:multitype:4.3.0'
// (3)解析json数据源的依赖
api 'com.google.code.gson:gson:2.8.5'
// (4)apache http网络库:使用EncodingUtils的utf-8转换的工具(将HTTP内容字符的字节数组转换为字符串)
implementation 'org.apache.httpcomponents:httpcore:4.4.4'

三、实现过程

1、解析json数据

(1)json数据源,放入assets文件夹下

如下json格式,将rv_data.json放入assets文件夹下,其中title就是展示标题的字段,show_type是用于区分type类型的字段,这里就展示了两个字段,用0和1进行简单区分。

{
   "data": [
      {
         "title": "item1.......",
         "show_type":0
      },
      {
         "title": "item2.......",
         "show_type":1
      },
      {
         "title": "item3.......",
         "show_type":0
      },
      ...
      
      {
         "title": "item17.......",
         "show_type":0
      }
    ]
}

(2)json数据源对应的RvDataBean

对应json数据源的DataBean如下所示:

TYPE_0、TYPE_1用于区分RecyclerView的Item类型、对应json数据源的show_type

@SerializedName("data") 是json数据最外层的data

@SerializedName("title")、 @SerializedName("show_type")同上

open class RvDataBean {
    companion object {
        const val TYPE_0 = 0 // 标题类型0
        const val TYPE_1 = 1 // 标题类型1
    }
    @SerializedName("data")
    private var mData: ArrayList<DataBean> = ArrayList<DataBean>()
    open fun getData(): ArrayList<DataBean> {
        return mData
    }
    open fun setData(data: ArrayList<DataBean>) {
        mData = data
    }
    class DataBean {
        @SerializedName("title")
        var title: String? = ""
        /**
         * 显示样式 0 、1
         */
        @SerializedName("show_type")
        var showType = 0
    }
}

(3)解析json数据,将json数据映射到RvDataBean对象上

两个步骤:读取json文件,转换为字符串;将字符串,转换为Bean对象

1)读取json文件,转换为字符串

public static String getJsonStrFromAssets(Context context, String fileName) {
    String cacheJson = null;
    InputStream in = null;
    try {
        in = context.getResources().getAssets().open(fileName);
        int length = in.available();
        byte[] buffer = new byte[length];
        int result = in.read(buffer);
        if (result == 0) {
            Log.e(TAG, "读取失败");
            return null;
        }
        cacheJson = EncodingUtils.getString(buffer, "UTF-8");
    } catch (Exception e) {
        Log.e(TAG, "ex", e);
    } finally {
        if (in != null) {
            try {
                in.close();
            } catch (IOException e) {
                Log.e(TAG, "ex", e);
            }
        }
    }
    return cacheJson;
}

2)将字符串,转换为Bean对象

public static <T> T convertJsonStrToBean(String json, final Class<T> clazz) {
    T dataBean = null;
    try {
        if (TextUtils.isEmpty(json)) {
            return null;
        }
        dataBean = new Gson().fromJson(json, clazz);
    } catch (Exception ex) {
        Log.e(TAG, "convertJsonToBean error = ", ex);
    }
    return dataBean;
}

有了前面的两个步骤,就可以调用获得RvDataBean对象了,如下

private suspend fun getData() : ArrayList<RvDataBean.DataBean> {
    return withContext(Dispatchers.IO) {
        val dataBean = arrayListOf<RvDataBean.DataBean>()
        val localData: String = StrUtils.getJsonFromAssets(AppApplication.application, "rv_data.json")
        dataBean.addAll(StrUtils.convertJsonToBean(localData, RvDataBean::class.java).getData())
        dataBean
    }
}

这里的AppApplication如下,便于更快捷获取context上下文;

class AppApplication : Application() {

    companion object {
        var application : AppApplication? = null
    }

    override fun onCreate() {
        super.onCreate()
        application = this
    }
}

<application
    android:name="com.example.nested_scroll_demo.AppApplication"
    ...

2、ViewModel

通过ViewModel来解析json数据源后,并通知RecyclerView去刷新界面。

(1)定义一个jsonList对象,当其内容变化时,即通知监听它的地方。

class RvViewModel : ViewModel() {

    var jsonList : MutableLiveData<ArrayList<RvDataBean.DataBean>?> = MutableLiveData()

    //从json里面读取消息
    @Nullable
    fun getDataFromJson() {
        MainScope().launch {
            if (jsonList.value == null) {
                jsonList.value = getData()
            }
        }
    }

    private suspend fun getData() : ArrayList<RvDataBean.DataBean> {
        return withContext(Dispatchers.IO) {
            val dataBean = arrayListOf<RvDataBean.DataBean>()
            val localData: String = StrUtils.getJsonFromAssets(AppApplication.application, "rv_data.json")
            dataBean.addAll(StrUtils.convertJsonToBean(localData, RvDataBean::class.java).getData())
            dataBean
        }
    }
}

(2)在Activity引入ViewModel,并监听jsonList

在MainActivity的onCreate中引入ViewModel,并主动解析json数据,当数据解析完成后,通过initObserve的jsonList变化通知Adapter去刷新界面,这里的adapter就是MulitAdapter,下面会讲解到。

private var rvViewModel : RvViewModel? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    rvViewModel = ViewModelProvider(this)[RvViewModel::class.java]
    initObserve()
    rvViewModel?.getDataFromJson()
}

@SuppressLint("NotifyDataSetChanged")
private fun initObserve() {
    // 列表数据内容刷新
    rvViewModel?.jsonList?.observe(this) {
        //live data两个特点:(1)黏性的可以接收到注册前的数据 ;(2)数据生命周期时间长; 因此这里要判空
        if (originData.isEmpty() && it != null) {
            originData.addAll(it)
            adapter.items = originData
            adapter.notifyDataSetChanged()
        }
    }
}

3、子item的Delegate实现

(1)第一个子item:TextDelegate

TextDelegate 继承 ItemViewDelegate, ItemViewDelegate两个构造参数:数据源DataBean以及视图模板ViewHolder

因此,做三件事:根据item布局来创建的ViewHolder模板、真正创建ViewHolder、数据绑定到ViewHolder上

class TextDelegate : ItemViewDelegate<RvDataBean.DataBean, TextDelegate.ViewHolder>() {

    private lateinit var mContext:  Context

    // 数据绑定到ViewHolder上
    override fun onBindViewHolder(holder: ViewHolder, item: RvDataBean.DataBean) {
        holder.titleTextView.text = item.title
    }

    // 真正创建ViewHolder的方法
    override fun onCreateViewHolder(context: Context, parent: ViewGroup): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.rv_item, parent, false)
        mContext = parent.context
        return ViewHolder(view)
    }

    // 根据item布局来创建的ViewHolder模板
    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val titleTextView : TextView = itemView.findViewById(R.id.rv_item_text)
    }
}

(2)第二个子item:ButtonDelegate

和第一个TextDelegate类似,这里不再展示。

4、MultiTypeAdapter和RecyclerView

(1)MainActivity的RecyclerView布局文件

布局特别简单,就是单纯的引入了一个RecyclerView,如下所示:

<?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"
    android:layout_marginLeft="20dp"
    android:layout_marginRight="20dp">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"/>


</androidx.constraintlayout.widget.ConstraintLayout>

(2)MultiTypeAdapter与RecyclerView的初始化及绑定

将所有的Item都注册到MultiTypeAdapter中,并将所有的Item与数据源进行一一映射,如下initRecyclerView中的代码所示,然后将adapter与RecyclerView绑定一起即可。

private val adapter = MultiTypeAdapter()

private var recyclerView : RecyclerView? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    rvViewModel = ViewModelProvider(this)[RvViewModel::class.java]
    initRecyclerView()
    initObserve()
    rvViewModel?.getDataFromJson()
}

private fun initRecyclerView() {
    // init rv
    recyclerView = findViewById(R.id.rv)
    recyclerView?.layoutManager = LinearLayoutManager(this)
    adapter.register(RvDataBean.DataBean::class.java).to(
        TextDelegate(),
        ButtonDelegate()
    ).withKotlinClassLinker { _, dataBean ->
        when (dataBean.showType) {
            RvDataBean.TYPE_0 -> TextDelegate::class
            RvDataBean.TYPE_1 -> ButtonDelegate::class
            else -> TextDelegate::class
        }
    }
    recyclerView?.adapter = adapter
}

四、其他

1、RecycelerView触边界后如何去掉波浪

在RecyclerView的布局中添加:android:overScrollMode="never",或者在代码层面调用recyclerView?.overScrollMode = View.OVER_SCROLL_NEVER,至于为什么滑动动顶部或者底部有波浪现象,留给下篇文章去探析下其中的原理吧。

2、Fragment页面如何加载ViewModel

Fragment页面和Activity页面加载ViewModel还是有点区别的,在Fragment中使用下面两个步骤即可:

(1)先构建ViewModelFactory

class RvViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        try {
            for (constructor in modelClass.constructors) {
                if (arrayOf(Context::class.java).contentEquals(constructor.parameterTypes)) {
                    return (constructor as Constructor<T>).newInstance(context)
                }
            }
            return modelClass.newInstance()
        } catch (e: InstantiationException) {
            throw RuntimeException("Cannot create an instance of $modelClass", e)
        } catch (e: IllegalAccessException) {
            throw RuntimeException("Cannot create an instance of $modelClass", e)
        }
    }
}

(2)然后调用下面方式即可创建viewModel

viewModel = ViewModelProvider(this, RvViewModelFactory(activity!!)).get(RvewModel::class.java)

3、在support包下如何使用MultiTypeAdapter进行register

使用support包下的RecyclerView 和 使用AndroidX下面的RecyclerView使用 MultiTypeAdapter是不同的,具体可以参考下面的写法,用于在support包下使用RecyclerView和MultiTypeAdapter。

尽量不要使用support包下的RecyclerView,因为MultiType已经不再针对适配support更新啦。

private fun initRecyclerView() {
    recyclerView = findViewById(R.id.rv)
    recyclerView?.layoutManager = LinearLayoutManager(this)
    adapter.register(RvDataBean.DataBean::class).to(
        TextDelegate(),
        ButtonDelegate()
    ).withKClassLinker { _, dataBean ->
        when (dataBean.showType) {
            RvDataBean.TYPE_0 -> TextDelegate::class
            RvDataBean.TYPE_1 -> ButtonDelegate::class
            else -> TextDelegate::class
        }
    }
    recyclerView?.adapter = adapter
}