• RecyclerView variety are many, the framework of layout of familiar BaseRecyclerViewAdapterHelper, vlayout, etc. Including Google in the new version of RecyclerView launched MergeAdapter(later renamed ConcatAdapter) and so on. Why write your own frameworks when there are so many out there? Most of the time these frameworks consider common generic scenarios, which may not be applicable to some bizarre product design requirements, which is why this article appears.

  • At present variety adapter framework for the overall design scheme has two kinds, one kind is BaseRecyclerViewAdapterHelper this extremely simplified developers to write code, use the most concise way to achieve the material. The other is a ConcatAdapter, where each style is designed as a separate module (sub-adapter), view creation, and data binding are handled within that module, and a main adapter wraps and ties those sub-adapters together. This paper adopts the second design method.

Because the amount of code is very small, the following will not say, directly paste code:

  1. View builder, keeping the native API naming
<ViewTypeCreator.kt>
abstract class ViewTypeCreator<T, VH : ViewHolder> {

    abstract fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): VH

    abstract fun onBindViewHolder(holder: VH.data: T)

    abstract fun match(data: T): Boolean

    open fun getItemId(position: Int) = RecyclerView.NO_ID

}
Copy the code
  1. Data adapter
<MultiTypeAdapter.kt>
abstract class MultiTypeAdapter : RecyclerView.Adapter<ViewHolder>() {
    private val dataCache: ArrayList<Class<*>> = ArrayList()
    private val creatorCache: SparseArray<SparseArray<ViewTypeCreator<Any, *>>> = SparseArray()
    private val viewTypeCache: SparseArray<ViewTypeCreator<Any, *>> = SparseArray()

    abstract fun getData(position: Int): Any

    inline fun <reified T : Any> registerCreator(creator: ViewTypeCreator<T, * >) {
        registerCreatorInner(T::class.java, creator)
    }

    fun registerCreatorInner(clazz: Class<*>, creator: ViewTypeCreator> < *, *) {
        var index = dataCache.indexOf(clazz)
        if (index == -1) {
            dataCache.add(clazz)
            index = dataCache.size - 1
        }
        var cache = creatorCache[index]
        if (cache == null) {
            cache = SparseArray()
        }
        val id = System.identityHashCode(creator)
        @Suppress("UNCHECKED_CAST")
        cache.put(id, creator as ViewTypeCreator<Any, *>)
        creatorCache.put(index, cache)
    }

    override fun getItemViewType(position: Int): Int {
        val data = getData(position)
        val viewType = getCreatorViewType(data)
        return if(viewType ! = -1) {
            viewType
        } else
            super.getItemViewType(position)
    }

    override fun getItemId(position: Int): Long {
        val itemViewType = getItemViewType(position)
        val viewCreator = getViewCreatorByViewType(itemViewType)
        return viewCreator.getItemId(position)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val viewCreator: ViewTypeCreator<*, *> = getViewCreatorByViewType(viewType)
        return viewCreator.onCreateViewHolder(LayoutInflater.from(parent.context), parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val data = getData(position)
        @Suppress("UNCHECKED_CAST")
        val viewCreator: ViewTypeCreator<Any, ViewHolder> =
            getViewCreatorByViewType(getItemViewType(position)) as ViewTypeCreator<Any, ViewHolder>
        viewCreator.onBindViewHolder(holder, data)}private fun getCreatorViewType(data: Any): Int {
        val clazz: Class<*> = data: :class.java
        var viewType: Int
        val index = dataCache.indexOf(clazz)
        if (dataCache.size > 0&& index ! = -1) {
            val creators: SparseArray<ViewTypeCreator<Any, *>> = creatorCache[index]
            // The Data bind more than one viewTypeCreator.
            if (creators.size() > 1) {
                creators.forEach { id, viewCreator ->
                    if (viewCreator.match(data)) {
                        viewType = id
                        if (viewTypeCache.indexOfKey(viewType) < 0) {
                            viewTypeCache.put(viewType, viewCreator)
                        }
                        return viewType
                    }
                }
            }
            // The Data only bind one viewTypeCreator.
            else if (creators.size() == 1) {
                viewType = creators.keyAt(0)
                if (viewTypeCache.indexOfKey(viewType) < 0) {
                    viewTypeCache.put(viewType, creators.valueAt(0))}return viewType
            }
        }
        throw RuntimeException("Current dataType [$clazz] is not found in DataTypeCache:\n$dataCache \nPlease check the Type of data for your custom creator.")}private fun getViewCreatorByViewType(viewType: Int): ViewTypeCreator<Any, *> {
        return viewTypeCache[viewType]
    }
}
Copy the code

Ok, so much code, here is a brief introduction to the principle:

  1. View builders don’t go into much detail here, but rather abstract the way views are builtmatchThis method:
fun match(data: T): Boolean
Copy the code
  • It receives a datatype parameter and needs to return oneBooleanValue, common product design common data type should correspond to a view type, but there is such a bizarre design, such as returning aPersonData type, ifsexShow a style for men (Left and right layout: the profile picture is on the left, and the introduction is on the right), ifsexWomen need to show a different style (Up and down layout: head in the middle, with a brief introduction below), such a scenario can define two views as follows:
<ManCreator.kt>
class ManCreator : ViewTypeCreator<Person, ManCreator.Holder>() {...override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_man, parent, false))}// The Creator creates the view when the Person is male
    override fun match(data: Person) = data.sex == Sex.MAN
}

<WomanCreator.kt>
class WomanCreator : ViewTypeCreator<Person, WomanCreator.Holder>() {...override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_female, parent, false))}// The view is created through this Creator when the Person is female
    override fun match(data: Person) = data.sex == Sex.WOMAN
}
Copy the code
  1. Adapters, of course, are inheritanceRecyclerView.Adapter
  • Let’s start with three caches:
<MultiTypeAdapter.kt>
abstract class MultiTypeAdapter : RecyclerView.Adapter<ViewHolder>() {
    // Store data types in data order
    private val dataCache: ArrayList<Class<*>> = ArrayList()// [DataType]
    // Store ViewTypeCreator, index is the subscript of the data type in the cache, since a data type can correspond to multiple creators,
    // Therefore use collection storage
    private val creatorCache: SparseArray<SparseArray<ViewTypeCreator<Any, *>>> = SparseArray()// DataTypeIndex - [ViewTypeCreators]
    // Store ViewTypeCreator, indexed to the corresponding viewType, one-to-one relationship, this cache is for quick lookup
    private val viewTypeCache: SparseArray<ViewTypeCreator<Any, *>> = SparseArray()// ViewType - ViewTypeCreator
}
Copy the code
  • Here’s how to register a view builder:
<MultiTypeAdapter.kt>
    // Automatically read the type of generic Data for storage
    inline fun <reified T : Any> registerCreator(creator: ViewTypeCreator<T, * >) {
        registerCreatorInner(T::class.java, creator)
    }

    fun registerCreatorInner(clazz: Class<*>, creator: ViewTypeCreator> < *, *) {
        var index = dataCache.indexOf(clazz)
        if (index == -1) {
            // If not, cache it
            dataCache.add(clazz)
            index = dataCache.size - 1
        }
        // Initializes the ViewTypeCreator collection corresponding to the Data type
        var cache = creatorCache[index]
        if (cache == null) {
            cache = SparseArray()
        }
        // Construct a unique identifier as an index
        val id = System.identityHashCode(creator)
        @Suppress("UNCHECKED_CAST")
        cache.put(id, creator as ViewTypeCreator<Any, *>)
        creatorCache.put(index, cache)
    }
Copy the code
  • Finally, in accordance with theRecyclerView.AdapterPrinciple of call process analysis:
<MultiTypeAdapter.kt>
    // Get the current index data
    abstract fun getData(position: Int): Any

    override fun getItemViewType(position: Int): Int {
        val data = getData(position)// 1. Obtain the data corresponding to the current index
        val viewType = getCreatorViewType(data)// 2. Get ViewType based on current data
        return if(viewType ! = -1) {
            viewType
        } else
            super.getItemViewType(position)
    }

    private fun getCreatorViewType(data: Any): Int {
        val clazz: Class<*> = data: :class.java// Get the class type of data
        var viewType: Int
        val index = dataCache.indexOf(clazz)// Find the index of this data in the cache
        if (dataCache.size > 0&& index ! = -1) {// Check whether the ViewTypeCreator corresponding to this data is registered
            val creators: SparseArray<ViewTypeCreator<Any, *>> = creatorCache[index]// Get the ViewTypeCreator set corresponding to this data
            // The Data bind more than one viewTypeCreator.
            if (creators.size() > 1) {// A data corresponds to multiple viewtypes
                creators.forEach { id, viewCreator ->
                    if (viewCreator.match(data)) {// Iterate over the ViewTypeCreator and determine if it matches the condition
                        viewType = id// viewType is the unique id obtained from the ViewTypeCreator instance above: system. identityHashCode
                        if (viewTypeCache.indexOfKey(viewType) < 0) {// If viewTypeCache is not cached, add it to the cache
                            viewTypeCache.put(viewType, viewCreator)// This level of cache is used to quickly find the corresponding ViewTypeCreator based on the viewType
                        }
                        return viewType
                    }
                }
            }
            // The Data only bind one viewTypeCreator.
            else if (creators.size() == 1) {// A data corresponds to a viewType
                viewType = creators.keyAt(0)
                if (viewTypeCache.indexOfKey(viewType) < 0) {
                    viewTypeCache.put(viewType, creators.valueAt(0))}return viewType
            }
        }
        throw RuntimeException("Current dataType [$clazz] is not found in DataTypeCache:\n$dataCache \nPlease check the Type of data for your custom creator.")}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Find the corresponding ViewTypeCreator according to the viewType, and build the Holer view with onCreateViewHolder
        val viewCreator: ViewTypeCreator<*, *> = getViewCreatorByViewType(viewType)
        return viewCreator.onCreateViewHolder(LayoutInflater.from(parent.context), parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // Find the corresponding ViewTypeCreator according to the viewType and bind the data via onBindViewHolder
        val data = getData(position)
        @Suppress("UNCHECKED_CAST")
        val viewCreator: ViewTypeCreator<Any, ViewHolder> =
            getViewCreatorByViewType(getItemViewType(position)) as ViewTypeCreator<Any, ViewHolder>
        viewCreator.onBindViewHolder(holder, data)}// Find the corresponding ViewTypeCreator according to viewType
    private fun getViewCreatorByViewType(viewType: Int): ViewTypeCreator<Any, *> {
        return viewTypeCache[viewType]
    }
Copy the code

The above isMultiTypeAdapterAll the code of

  1. Here’s a use case:
  • Start by defining an adapter, inheritanceMultiTypeAdapter
class SampleAdapter : MultiTypeAdapter() {

    val data = mutableListOf<Any>()

    override fun getData(position: Int) = data[position]

    override fun getItemCount(a) = data.size
}
Copy the code
  • Define two display text and one display pictureviewType
// Literal data type definition: contains the main title and subtitle
data class Title(val mainTitle: String = "".val subTitle: String = "")
// Text style 1
class MainTitleCreator : ViewTypeCreator<Title, MainTitleCreator.Holder>() {
    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val title: TextView = itemView.findViewById(R.id.main_title)
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_main_title, parent, false))}override fun onBindViewHolder(holder: Holder.data: Title) {
        holder.title.text = data.mainTitle
    }

    override fun match(data: Title): Boolean {
        return! TextUtils.isEmpty(data.mainTitle) && TextUtils.isEmpty(data.subTitle)
    }
}
// Text style 2
class SubTitleCreator : ViewTypeCreator<Title, SubTitleCreator.Holder>() {
    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val title: TextView = itemView.findViewById(R.id.sub_title)
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_sub_title, parent, false))}override fun onBindViewHolder(holder: Holder.data: Title) {
        holder.title.text = data.subTitle
    }

    override fun match(data: Title): Boolean {
        return! TextUtils.isEmpty(data.subTitle) && TextUtils.isEmpty(data.mainTitle)
    }
}

// Image style
class ImageCreator : ViewTypeCreator<Int, ImageCreator.Holder>() {
    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val image: ImageView = itemView.findViewById(R.id.image)
    }

    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
        return Holder(inflater.inflate(R.layout.view_type_image, parent, false))}override fun onBindViewHolder(holder: Holder.data: Int) {
        holder.image.setImageResource(data)}override fun match(data: Int): Boolean {
        return false}}Copy the code
  • registeredviewTypeCreator
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        recycler_view.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
        val adapter = SampleAdapter().apply {
            // image type
            registerCreator(ImageCreator())
            // text type
            registerCreator(TextCreator())
            // the same bean but different view type
            registerCreator(MainTitleCreator())
            registerCreator(SubTitleCreator())
        }
        fillData(adapter)
        recycler_view.adapter = adapter
    }

    private fun fillData(adapter: SampleAdapter) {
        for (i in 0.10.) {
            adapter.data.run {
                add(R.drawable.test)
                add("I am string")
                add(Title("I am MainTitle"))
                add(Title(""."I am SubTitle"))}}}Copy the code

The final display effect is as follows:

If a new style is added later in the product design, you just need to define a new ViewTypeCreator, register it with a MultiTypeAdapter, and then add the corresponding type of data to the data set. Adapter and data, view build decouple.

Project address: github.com/seagazer/mu…