当前位置:   article > 正文

Android进阶之路 - ConcatAdapter到底有没有学习必要?_setisolateviewtypes

setisolateviewtypes

去年看项目时发现挺多 RecyclerView列表 加载不同类型视图采用了 ConcatAdapter,当时想了想印象中没有这个工具啊,后来才发现又是以前欠的债,对于我来说可能是新知识,但相对现在它其实已经有段年纪了,蹭着最近有时间补一下学习心得

先说明一下我的看法,针对ConcatAdapter到底有没有学习必要?我的答案:我觉得可以学,但是没有太大必要,锦上添花而已

当我搜索 ConcatAdapter 相关内容时,发现挺多内容长得一模一样,基本都是搬得最早的一篇,稍加学习后结合官网+源码方式提出我自己的看法,如有不足,欢迎指正

不知道你有没有和我相同的疑问:在ConcatAdapter未出现前,我们也可以在同列表加载不同的View样式,那为什么还要在推出ConcatAdapter呢?

在这里插入图片描述

Tip:今天理论先行吧,免得着急用,却选错了方案
在此之前我们如果需要在RecyclerView列表中加载多种不同类型的视图时,采用的是同一个Adapter,主要重写其getItemViewType 方法,然后在onCreateViewHolderonBindViewHolder 做逻辑处理,其实这种方式完全可以满足我们的业务场景;

之所以推出ConcatAdapter只是为了实现高内聚、低耦合的效果,同时其单一职责,方便复用;假设公共Adapter中业务逻辑复杂,且视图不会穿插显示,那么将不同ViewType逻辑进行剥离,然后组装成新的Adapter会更利于维护

从穿插效果来看我们到底选哪种方案,在RecyclerView

  • ItemView 需要穿插展示,并无固定规律,类似以ABAC形式展示,最好使用之前的方式,兼容性更强,容错率更高
  • ItemView 展示效果固定,类似以ABC这样顺序展示,可以选择采用ConcatAdapter

有的朋友可能想着通过ConcatAdapter动态去组装itemView以实现部分穿插效果,其实我个人并不建议,首先ConcatAdapter内有Config缓存池的概念,会导致你穿插效果不太理想;同时随着随机性变大,很难去完美的动态组装ConcatAdapter效果

基础简介

通过 Google 国内官网来看 ConcatAdapter 是从2021年4月7日发布的RecyclerView1.2.0版本后才加入的

根据下图官网图例,我们可以看出

  • ConcatAdapter作用于RecyclerView1.2.0及后续版本
  • ConcatAdapter依旧继承于RecyclerView.Adapter
  • 关于ConcatAdapter的简单使用示例
    在这里插入图片描述

答疑解析

仅记录我在写该Demo时遇到的一些小插曲

引用不到 ConcatAdapter?

不知道你一开始有没有发现调用不到 ConcatAdapter … 其实是因为刚创建的项目内自带的RecyclerView版本还没到1.2.1

build.app

    implementation 'androidx.recyclerview:recyclerview:1.2.1'
  • 1

LinearLayoutManager 怎么控制方向?

LinearLayoutManager来控制展示方向,可以用构造方法,也可以直接用实例调用内部方法

默认垂直显示

   var linearLayoutManager = LinearLayoutManager(this)
  • 1

可以手动设置方向

   var linearLayoutManager = LinearLayoutManager(this)
   linearLayoutManager.orientation=LinearLayoutManager.VERTICAL
  • 1
  • 2

LinearLayoutManager 构造方法

在这里插入图片描述

ConcatAdapter 到底用哪个构造方法?

ConcatAdapter 多种构造方式,针对于 Config 可传可不传,具体看场景

在这里插入图片描述

ConcatAdapter 的Config有何用?

ConcatAdapter 的构造方法中可以随处可见Config,这个应该可以算是其自由特性,如无特殊需求无需设置

通过Config调用提示可以看出它的主要方法

在这里插入图片描述

Config 相关源码并不多,主要就是这俩个成员变量

在这里插入图片描述

isolateViewTypes 缓存,根据注释翻译+自我理解,结果如下

  • true不共用一级缓存,都拥有自己的二级缓存(二级缓存间同类共用):ConcatAdapter 在适配器内隔离视图类型,防止它们使用相同的RecyclerView.ViewHolders
  • false共用一级缓存:ConcatAdapter假定所有分配的适配器共享一个全局视图类型池,以便它们使用相同的视图类型来引用相同的RecyclerView.ViewHolders;将允许嵌套适配器共享RecyclerView。但这也意味着这些适配器不应该有冲突的视图类型(RecyclerView.Adapter.getItemViewType(int)),这样两个不同的适配器为不同的RecyclerView.ViewHolders返回相同的视图类型;简而言之就是表示如果viewType相同,那么它们将共用一个缓存池

Tip:我把isolateViewTypes中true场景看做二维数组(兼容性强,每一个item相互独立且唯一),false场景看做一维数组(如果组内采用的viewHodler不同那么就会报错,反之会报转换错误)

isolateViewTypes为 false 场景模拟(可正常显示视图叠加效果)

   val firstAdapter = FirstAdapter(firstDataList)
   val secondAdapter = FirstAdapter(firstDataList)
   var config = ConcatAdapter.Config.Builder()
       .setIsolateViewTypes(false)
       .build()
   val concatAdapter = ConcatAdapter(config, firstAdapter, secondAdapter)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

stableIdMode:定义ConcatAdapter是否应该支持稳定id (RecyclerView.Adapter.hasStableIds()) - 我看别人记录的getItemViewType坑的问题,或许可以通过这里尝试解决,因为时间未必我就不赘述了

  • NO_STABLE_IDS:在此模式下,ConcatAdapter忽略子适配器上报的稳定id。这是默认模式。
  • ISOLATED_STABLE_IDS:在这种模式下,ConcatAdapter将从hasStableIds()返回true,并且将需要所有添加的RecyclerView。适配器必须具有稳定的id。由于两个不同的适配器可能会返回相同的稳定id,因为它们彼此不知道,因此ConcatAdapter将隔离每个RecyclerView。适配器的id池,以便在向RecyclerView报告之前覆盖报告的稳定id。在这种模式下,从RecyclerView.ViewHolder.getItemId()返回的值可能与从RecyclerView.Adapter.getItemId(int)返回的值不同。ConcatAdapter.Config.StableIdMode。
  • SHARED_STABLE_IDS:在这种模式下,ConcatAdapter将从hasStableIds()返回true,并且将需要所有添加的RecyclerView。适配器必须具有稳定的id。不像ConcatAdapter.Config.StableIdMode。ISOLATED_STABLE_IDS, ConcatAdapter不会覆盖返回的项id。在此模式下,子RecyclerView。适配器必须知道彼此,并且永远不会返回相同的id,除非在RecyclerView.Adapters之间移动了一个项目。默认值为“ConcatAdapter.Config.StableIdMode.NO_STABLE_IDS”。

StableIdMode 对应模式,源码解释

在这里插入图片描述

ConcatAdapter 怎么增、删多个Adapter?

这俩种方式在实战场景中各有千秋,一个是固定知道有多少视图样式,另一个是只有某些条件触发才会增删内部Adapter~

通过构造方法传入多Adapter

   val concatAdapter = ConcatAdapter(config,firstAdapter, secondAdapter)
  • 1

通过内部方法传入多Adapter

   val concatAdapter = ConcatAdapter()
   concatAdapter.addAdapter(firstAdapter)
   concatAdapter.addAdapter(secondAdapter)
  • 1
  • 2
  • 3

addAdapter方法提供了将Adapter插入到指定位置的重载方法

在这里插入图片描述

删除某一视图对应的Adapter

   val concatAdapter = ConcatAdapter()
   concatAdapter.addAdapter(firstAdapter)
   concatAdapter.removeAdapter(firstAdapter)
  • 1
  • 2
  • 3

开发实践

如果只是为了开发使用的话,直接看调用实践(MainActivity)即可

子View就绪

因为涉及到列表多样式,所以首先准备一些对应 itemLayoutAdapter

RecyclerViewItemView之 - FirstAdapter

package com.example.concatadatper

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class FirstAdapter(private val dataList: MutableList<String>) : RecyclerView.Adapter<FirstAdapter.FirstViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FirstViewHolder {
        var inflateView = LayoutInflater.from(parent.context).inflate(R.layout.item_first, parent, false)
        return FirstViewHolder(inflateView)
    }

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

    override fun onBindViewHolder(holder: FirstViewHolder, position: Int) {
        holder.firstText.text = dataList[position]
    }

    class FirstViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var firstText: TextView

        init {
            firstText = itemView.findViewById<TextView>(R.id.tv_first)
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

item_first

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:background="#f65478"
        android:textColor="#ffffff"
        android:id="@+id/tv_first"
        android:text="First" />
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

RecyclerViewItemView之 - SecondAdapter

package com.example.concatadatper

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class SecondAdapter(private val dataList: MutableList<String>) : RecyclerView.Adapter<SecondAdapter.SecondViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SecondViewHolder {
        var inflateView = LayoutInflater.from(parent.context).inflate(R.layout.item_second, parent, false)
        return SecondViewHolder(inflateView)
    }

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

    override fun onBindViewHolder(holder: SecondViewHolder, position: Int) {
        holder.secondText.text = dataList[position]
    }

    class SecondViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var secondText: TextView

        init {
            secondText = itemView.findViewById<TextView>(R.id.tv_second)
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

item_second

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:background="#e98745"
        android:textColor="#ffffff"
        android:id="@+id/tv_second"
        android:text="Second" />
</LinearLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

调用实践

package com.example.concatadatper

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {
    private var firstDataList: MutableList<String> = mutableListOf<String>()
    private var secondDataList: MutableList<String> = mutableListOf<String>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        initData()

        val firstAdapter = FirstAdapter(firstDataList)
        val secondAdapter = SecondAdapter(secondDataList)
        var config = ConcatAdapter.Config.Builder()
            .setIsolateViewTypes(true)
            .build()

        val concatAdapter = ConcatAdapter(config, firstAdapter, secondAdapter)

        val mRv = findViewById<RecyclerView>(R.id.rv)
        mRv.layoutManager = LinearLayoutManager(this)
        mRv.adapter = concatAdapter
    }

    /**
     * 数据模拟
     * */
    private fun initData() {
        for (data in 0..10) {
            firstDataList.add("First-Data-$data")
        }
        for (data in 0..10) {
            secondDataList.add("Second-Data-$data")
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

activity_main

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat 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.recyclerview.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</androidx.appcompat.widget.LinearLayoutCompat>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

小小兴趣

不知道你有没有像我一样的想法,能否通过ConcatAdapter实现类似viewitem穿插的效果?

期望效果:列表中子视图以ABA效果展示

伪代码:IsolateViewTypes = true 不共享缓存,可正常显示ABA

   val firstAdapter = FirstAdapter(firstDataList)
   val secondAdapter = SecondAdapter(secondDataList)
   val firstAdapter1 = FirstAdapter(firstDataList)//重新创建一个对应的Adapter
   var config = ConcatAdapter.Config.Builder()
       .setIsolateViewTypes(true)
       .build()
   val concatAdapter = ConcatAdapter(config, firstAdapter, secondAdapter,firstAdapter1 )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

伪代码:IsolateViewTypes = false 共享缓存,直接崩了,类型转换失败,错误如下图

   val firstAdapter = FirstAdapter(firstDataList)
   val secondAdapter = SecondAdapter(secondDataList)
   var config = ConcatAdapter.Config.Builder()
       .setIsolateViewTypes(false)
       .build()
   val concatAdapter = ConcatAdapter(config, firstAdapter, secondAdapter,firstAdapter)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

有的人可能会想以 ConcatAdapter(config, firstAdapter, secondAdapter,secondAdapter) 方式添加,结果依旧如下

在这里插入图片描述

假设期望列表中子视图以ABA效果展示为AB,可能存在一些问题

如果同列表、同Adapter再次添加到ConcatAdapter后仅显示一次就需要设置IsolateViewTypes = false,这样重复添加的Adapter直接读取了首个Adapter内的ViewHodler缓存;虽然解决了重复问题,不过就会导致无法兼容另一个itemTypeAdapter,真是鱼和熊掌不可兼得。当然可能有办法,只是我尚不自知~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/286592
推荐阅读
相关标签
  

闽ICP备14008679号