App interfaces are getting more complex, with more elements, and even a large phone screen can’t fit everything in one screen. Organizing different types of elements into recyclerViews can transcend the limitations of screens. There are many pain points in the use of RecyclerView, such as “how to deal with the entry and its child control click events?” “, “How to add new types to a list without getting mad?” “, “How to refresh the list inexpensively? “, “How do I preload the next screen of data? And so on. This article tries to make it easy to extend list data types.

Single type list

When the project started, the news list was not complex. It was a simple Feed stream with images on the left and titles on the right. The Adapter implementation was as simple:

// News adapter
class NewsAdapter : RecyclerView.Adapter<NewsViewHolder>() {
	// News list
    var news: List<News>? = null
        set(value) {
            field = value
            notifyDataSetChanged()
        }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
    	// Build the news table entry
        val itemView = ...// Omit the build details
        return NewsViewHolder(itemView)
    }

    override fun getItemCount(a): Int {
        returnnews? .size ? :0
    }

    override fun onBindViewHolder(holder: NewsViewHolder, position: Int){ news? .getOrNull(position)? .let { holder.bind(it) } } }/ / ViewHolder news
class NewsViewHolder(itemView: View):RecyclerView.ViewHolder(itemView) {
    fun bind(news:News){
    	// Populate the news entry view with news items
        itemView.apply {... // omit binding details}}}// News entity class
data class News(
    @SerializedName("image") varimage: String? .@SerializedName("title") var title: String?
)
Copy the code
  1. Defining entity Classes
  2. Defines the object bound to the entity classViewHolder
  3. Define andViewHolderThe binding ofAdapter

When building RecyclerView, most follow such steps.

A list of empty views

Lists that show web content often have an empty view to alert the user if a network request fails.

The NewsAdapter defined just now has been coupled with NewsViewHolder and cannot meet the requirement of adding a list empty view. Refactor as follows:

// List adapter base class
abstract class BaseRecyclerViewAdapter<T> : RecyclerView.Adapter<BaseViewHolder?> {
    companion object {
        const val TYPE_EMPTY_VIEW = -1 / / the empty view
        const val TYPE_CONTENT = -2 // Non-empty view
    }
	// List data
    protected var datas: MutableList<T>? = null
	/ / the empty view
    private var emptyView:View? = null
	// Current list state (empty or not empty)
    private var currentType = 0
    // Check whether the list is empty
    private inner class DataObserver : RecyclerView.AdapterDataObserver() {
        override fun onChanged(a) {
        	// Update the current list status according to the list data length
            currentType = if(datas ! =null&& datas.size ! =0) {
                TYPE_CONTENT
            } else {
                TYPE_EMPTY_VIEW
            }
        }
    }
    // For subclass implementation to define how to build table entries
    protected abstract fun createHolder(parent: ViewGroup? , viewType:Int, inflater: LayoutInflater?).: BaseViewHolder
    // For subclasses to define how to bind data to the entry view
    protected abstract fun bindHolder(holder: BaseViewHolder? , position:Int)
    // For subclasses to define the actual entry length
    protected abstract val count: Int
    // For subclasses to define the real entry type
    protected abstract fun getViewType(position: Int): Int

    constructor(context: Context) {
        this.context = context
        // Listen for list data changes
        registerAdapterDataObserver(DataObserver())
    }
    
    fun setData(datas: MutableList<T>? {
        this.datas = datas
        notifyDataSetChanged()
    }
	// Inject an empty view
    fun setEmptyView(emptyView: View) {
        this.emptyView = emptyView
    }
	// Create an entry view
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
        val viewHolder: BaseViewHolder
        when (viewType) {
            TYPE_EMPTY_VIEW -> { viewHolder = BaseViewHolder(emptyView) }
            // Non-empty table entry construction logic is deferred to subclass implementation
            else -> viewHolder = createHolder(parent, viewType)
        }
        return viewHolder
    }
	// Bind data to the entry view
    override fun onBindViewHolder(holder: BaseViewHolder? , position:Int) {
        val viewType = getItemViewType(position)
        when (viewType) {
        	// Empty views do not need to be bound
            TYPE_EMPTY_VIEW -> {}
            // Non-empty view binding logic is deferred to subclass implementation
            else -> bindHolder(holder, position)
        }
    }

    override fun getItemCount(a): Int {
    	// If the list is empty, the length of the list is 1 (which sounds awkward), otherwise the actual length of the list is returned
        return if (currentType == TYPE_EMPTY_VIEW) { 1 } else count
    }

    override fun getItemViewType(position: Int): Int {
    	If the list is empty, the empty list type is returned, otherwise the real column entry type is returned
        return if (datas == null || datas.size == 0) {
            TYPE_EMPTY_VIEW
        } else {
            getViewType(position)
        }
    }
}

/ / ViewHolder base class
public class BaseViewHolder extends RecyclerView.ViewHolder {
    public BaseViewHolder(View itemView) { super(itemView); }}Copy the code

Abstract a base class BaseAdapter, It adds if-else branches to empty view logic in onCreateViewHolder(), onBindViewHolder(), getItemCount(), and getItemViewType(), respectively. Subclasses need to implement the corresponding four abstract methods to define business table entries and can inject an empty view through setEmptyView().

BaseAdapter also introduces the ViewHolder base class, which eliminates the need to bind to a specific ViewHolder when constructing the Adapter, making it easy to extend list data types.

But there is a bit of a twist to this scheme, as you can see from the implementation of getItemCount() :

override fun getItemCount(a): Int {
	// If the list is empty, the length of the list is 1 (which sounds awkward), otherwise the actual length of the list is returned
    return if (currentType == TYPE_EMPTY_VIEW) { 1 } else count
}
Copy the code

The catch is that an empty view is an entry in a list, but it “specialises” its handling. And specialization via if-else is not extensible! If you wanted to add headers and footers to your list, you would have to modify the BaseAdapter class to add more if-else branches.

Pseudo multitype list

As the version iterations, you need to insert the Banner at the top of the list and scroll along with the news.

BaseAdapter is not extension-friendly, but it works fine (it handles empty view logic, after all). This time the new requirement can be extended by adding if-else to the subclass:

// News adapter
class NewsAdapter : BaseRecyclerViewAdapter<News>() {
	val TYPE_NEWS = 1
    val TYPE_BANNER = 2
	// Add a new type to the list with if-else
    override fun createHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
    	if(viewType == TYPE_NEWS) {return createNewsViewHolder(parent)
        }else {
        	return createBannerViewHolder(parent)
        }
    }
	// Bind data to different entries using if-else
    override fun bindHolder(holder: BaseViewHolder, position: Int) {
    	val viewType = getViewType(position)
        if(viewType == TYPE_NEWS) {(holder)as? NewsViewHolder)? .bind(datas[position]) }else {
        	(holder as? BannerViewHolder)? .bind(datas[position]) } }override val count: Int
        protected get() = if (datas == null) 0 else datas.size
    
    // Build the Banner entry
    private fun createBannerViewHolder(parent: ViewGroup): BaseViewHolder {
    	val itemView = ... // Omit the build details
        return BannerViewHolder(itemView)
    }
    
    // Build the news entry
    private fun createNewsViewHolder(parent: ViewGroup): BaseViewHolder {
    	val itemView = ... // Omit the build details
        return NewsViewHolder(itemView)
    }
}

/ / ViewHolder news
class NewsViewHolder(itemView: View): BaseViewHolder(itemView) {
    fun bind(news:News){...// omit binding details}
}

// Banner ViewHolder
class BannerViewHolder(itemView: View): BaseViewHolder(itemView) {
    fun bind(news:News){...// omit binding details}
}

// News entity class (added banner field)
data class News(
    @SerializedName("image") varimage: String? .@SerializedName("title") vartitle: String? .var banners: List<Banner>?
)

// Banner entity class (returned from a different interface than the news interface)
data class Banner(
	@SerializedName("jumpUrl") varjumpUrl: String? .@SerializedName("imageUrl") var imageUrl: String?
)
Copy the code

If RecyclerView is a project like this, every time you add a new type to it, you’re going crazy.

  • Because the NewsAdapter is coupled to the concrete News entity class, the new data type can only be a member of News, and while a new type is added to the list from a business perspective, the code combines the two types into one (pseudo-multitype). If you add n types, the News class adds n members, and only one of the fields is used by each entry. The rest of the fields are redundant.

  • Because entries of different types are distinguished by if-else, the NewsAdapter class must be modified every time a new type is added. Changes to existing classes are dangerous because they may have been patched by multiple people and become “potholes,” full of unspoken “rules” that can “explode mines” if you’re not careful.

Crazy is foreplay, inexplicable bugs are the climax. As you must have noticed, adding a new type to a list in this way breaks the logic of the base class’s hollow view:

abstract class BaseRecyclerViewAdapter<T> : RecyclerView.Adapter<BaseViewHolder?> {
    // Check whether the list is empty
    private inner class DataObserver : RecyclerView.AdapterDataObserver() {
        override fun onChanged(a) {
        	// If the list has no data, load the empty view, otherwise load the business data
            currentType = if(datas ! =null&& datas.size ! =0) {
                TYPE_CONTENT
            } else {
                TYPE_EMPTY_VIEW
            }
        }
    }
}
Copy the code

When the banner interface returns data and the news interface returns null, the list length is not 0, so the BaseAdapter does not display an empty view. An empty view of the news area when the product expects no news, and an empty view of the entire list when neither banner nor news is displayed. Continue refactoring base classes? (Really want to delete this base class)

Type independent adapter

Existing Adapters are difficult to extend and cause bugs when they are extended because the Adapter is coupled to a specific data type.

Is it possible to design an adapter that is type-independent? Something like this:

class VarietyAdapter(
    var dataList: MutableList<Any> = mutableListOf()
) : RecyclerView.Adapter<ViewHolder>() { }

// Build a list of three data types, showing a news item, a Banner, and an image
val adapter = VarietyAdapter().apply {
	dataList = mutableListOf (
    	News(),
        Banner(),
        "https:xxx")}Copy the code

The VarietyAdapter declaration avoids all type-related information:

  • originallyRecyclerView.AdapterClass must declare a concreteViewHolderType, which is used directly hereRecyclerView.ViewHolderThe base class.
  • originallyRecyclerView.AdapterIn thedatasMust be a list of specific data types, where all non-null type base classes are used directlyAny.

When adding data to a VarietyAdapter, the different types of data are rubbed together in a list.

A new Adapter typically implements the following three methods:

class VarietyAdapter(
    var datas: MutableList<Any> = mutableListOf()
):RecyclerView.Adapter<RecyclerView.ViewHolder>() {
	// Build the entry layout
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {}
	// Fill in the entry content
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {}
	// Get the number of entries
    override fun getItemCount(a): Int{}}Copy the code

The implementations of onCreateViewHolder() and onBindViewHolder() are bound to the entry’s data type.

The abstract actions of building and filling entries do not change with service changes. However, the specific actions of building and filling entries change with service changes.

The mistake made earlier was that the Adapter handled the “concrete actions” itself, which made it difficult to scale and made the Adapter change as the business changed.

Can “concrete actions” be taken out of the Adapter and handed over to other actors, and the Adapter only deals with abstractions? This improves The Scalability of the Adapter.

// A set of policies similar to Adapter
abstract class Proxy<T, VH : RecyclerView.ViewHolder> {
    // Build the entry
    abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
    // Fill in the entry
    abstract fun onBindViewHolder(holder: VH.data: T, index: Int, action: ((Any?). ->Unit)? = null)
    // Fill the entry (partial refresh)
    open fun onBindViewHolder(holder: VH.data: T, index: Int, action: ((Any?). ->Unit)? = null, payloads: MutableList<Any>) {
        onBindViewHolder(holder, data, index, action)
    }
}
Copy the code

Recyclerview. Adapter: recyclerView. Adapter: recyclerView. Adapter: recyclerView. Adapter: RecyclerView.Adapter: RecyclerView.Adapter: RecyclerView.Adapter: RecyclerView.Adapter But it does not inherit recyclerView. Adapter directly, that is, it does not have the function of complete recyclerView. Adapter, so it can not be called the proxy mode, but should be the policy mode.

The policy class defines two type parameters, the first T representing the type of the data corresponding to the entry and the second VH representing the type of the ViewHolder entry.

A policy class is abstract; each instance of it represents an entry of a type and corresponds to a data type.

An instance of a policy class usually looks like this:

// Text class entry strategy (the corresponding data is Text, the corresponding ViewHolder is TextViewHolder)
class TextProxy : Proxy<Text, TextViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    	// Build the table view (build the layout DSL)
        val itemView = parent.context.run {
            TextView {
                layout_id = "tvName"
                layout_width = wrap_content
                layout_height = wrap_content
                textSize = 40f
                gravity = gravity_center
                textColor = "#ff00ff"}}// Construct the entry ViewHolder
        return TextViewHolder(itemView)
    }

	// Bind entry data
    override fun onBindViewHolder(holder: TextViewHolder.data: Text, index: Int, action: ((Any?). ->Unit)?{ holder.tvName? text =data.text
    }
}

// The "literal data" corresponding to the literal entry
data class Text( var text: String )

// Text class entry ViewHolder
class TextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val tvName = itemView.find<TextView>("tvName")}Copy the code

When you build the Proxy, you specify the data and ViewHolder corresponding to the table entry and define them in the same Kotlin file for easy modification.

The DSLS used to build the layout can be found here.

Recyclerview. Adapter will hold a group of data and several instances of policy classes at the same time, its role is to distribute the “build table” and “fill table” tasks to the corresponding policy classes according to the data type.

class VarietyAdapter(
    // Policy list
    private var proxyList: MutableList<Proxy<*, *>> = mutableListOf(),
    // Data list
    var dataList: MutableList<Any> = mutableListOf()
) : RecyclerView.Adapter<ViewHolder>() {
    // Injection policy
    fun <T, VH : ViewHolder> addProxy(proxy: Proxy<T, VH>) {
        proxyList.add(proxy)
    }
    // Remove the policy
    fun <T, VH : ViewHolder> removeProxy(proxy: Proxy<T, VH>) {
        proxyList.remove(proxy)
    }
    // Distribute the build table layout to the policy
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return proxyList[viewType].onCreateViewHolder(parent, viewType)
    }
    // Distribute the fill entry to the policy
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        (proxyList[getItemViewType(position)] as Proxy<Any, ViewHolder>).onBindViewHolder(holder, dataList[position], position, action)
    }
    // Distribute the fill entry to the policy (layout refresh)
    override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
		(proxyList[getItemViewType(position)] as Proxy<Any, ViewHolder>).onBindViewHolder( holder, dataList[position], position, action, payloads )
    }
    // Returns the total amount of data
    override fun getItemCount(a): Int = dataList.size
	// Get the entry type
    override fun getItemViewType(position: Int): Int {
        return getProxyIndex(dataList[position])
    }
    // Get the policy index in the list
    private fun getProxyIndex(data: Any): Int = proxyList.indexOfFirst {
    	// If the first type parameter T in Proxy
      
        is of the same type as the data, the index of the corresponding policy is returned
      ,vh>
        (it.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0].toString() == data.javaClass.toString()
    }
    // Abstract policy class
    abstract class Proxy<T, VH : ViewHolder> {
        abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
        abstract fun onBindViewHolder(holder: VH.data: T, index: Int, action: ((Any?). ->Unit)? = null)
        open fun onBindViewHolder(holder: VH.data: T, index: Int, action: ((Any?). ->Unit)? = null, payloads: MutableList<Any>) {
            onBindViewHolder(holder, data, index, action)
        }
    }
}
Copy the code

VarietyAdapter stores a set of policies in an ArrayList structure: var proxyList: MutableList = mutableListOf() contains a star projection of the two types that must be specified by Proxy, so that any type can be a subtype of Proxy<*,*>. (For more on type projection, click here.)

The method getItemViewType(), which originally returned the type of the entry, now returns the index value of the policy so that the policy corresponding to the data can be found in the proxyList and delegated when the entry is built and bound.

The index value of the policy is traversed through the proxyList and compared with the data type of each policy. The comparison content is “whether the first type parameter of the policy is consistent with the data type”. If so, it indicates that the policy is the policy corresponding to the specified data.

(proxy.javaClass.genericSuperclass as ParameterizedType)// Get the type parameter list
	.actualTypeArguments[0].toString()// Get the first type in the type parameter table column
Copy the code

For instance Proxy of a generic class Proxy

, the expression above returns the full class name of the first type parameter T of Proxy.
,>

You can then use a VarietyAdapter like this:

// Build the adapter
val varietyAdapter = VarietyAdapter().apply {
    // Add two policies to the Adapter to display text and images respectively
    addProxy(TextProxy())
    addProxy(ImageProxy())
    // Build data (merge different data types into a list)
    dataList = mutableListOf(
    	Text("item 1"), // Represents a literal entry
    	Image("#00ff00"), // Represents the picture entry
    	Text("item 2"),
    	Text("item 3"),
    	Image("#88ff00")
	)
    notifyDataSetChanged()
}
// Assign Adapter to RecyclerViewrecyclerView? .adapter = varietyAdapter recyclerView? .layoutManager = LinearLayoutManager(this)
Copy the code

Among them, ImageProxy and Image are similar to TextProxy and Text mentioned above.

Single type multiple match

Sometimes the list data returned by the server has a Type field that indicates which layout the client should present, that is, multiple layouts for the same data type. VarietyAdapter’s existing approach does not meet this requirement because the matching rule is written into the getProxyIndex() method.

To extend the matching rule, add the following interface:

// The mapping between data and policy
interface DataProxyMap {
    // Convert the data to the policy class name
    fun toProxy(a): String
}
Copy the code

Then let the data class implement this interface:

data class Text(
    var text: String,
    var type: Int // Specify the layout type with type
) : VarietyAdapter.DataProxyMap {
    override fun toProxy(a): String {
        return when (type) {
            1 -> TextProxy1::class.java.toString() // Type 1 corresponds to TextProxy1
            2 -> TextProxy2::class.java.toString() // Type 2 corresponds to TextProxy2
            else -> TextProxy2::class.java.toString()
        }
    }
}
Copy the code

You also need to modify the getProxyIndex() method:

private fun getProxyIndex(data: Any): Int = proxyList.indexOfFirst {
    // Gets the class name of the first type parameter in the policy class
    val firstTypeParamClassName = (it.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0].toString()
    // Get the policy class name
    val proxyClassName = it.javaClass.toString()
    // The first type parameter of the policy class is the same as the data type
    firstTypeParamClassName == data.javaClass.toString()
    		// Minor matching condition: The name of the user-defined matching policy of the data class is the same as the name of the current policy
            && (data as? DataProxyMap)? .toProxy() ? : proxyClassName == proxyClassName }Copy the code

trailer

The next article will extend the VarietyAdapter to make refreshing lists more efficient.

Talk is cheap, show me the code

Recommended reading

RecyclerView series article directory is as follows:

  1. RecyclerView caching mechanism | how to reuse table?

  2. What RecyclerView caching mechanism | recycling?

  3. RecyclerView caching mechanism | recycling where?

  4. RecyclerView caching mechanism | scrap the view of life cycle

  5. Read the source code long knowledge better RecyclerView | click listener

  6. Proxy mode application | every time for the new type RecyclerView is crazy

  7. Better RecyclerView table sub control click listener

  8. More efficient refresh RecyclerView | DiffUtil secondary packaging

  9. Change an idea, super simple RecyclerView preloading

  10. RecyclerView animation principle | change the posture to see the source code (pre – layout)

  11. RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache

  12. RecyclerView animation principle | how to store and use animation attribute values?

  13. RecyclerView list of interview questions | scroll, how the list items are filled or recycled?

  14. RecyclerView interview question | what item in the table below is recycled to the cache pool?

  15. RecyclerView performance optimization | to halve load time table item (a)

  16. RecyclerView performance optimization | to halve load time table item (2)

  17. RecyclerView performance optimization | to halve load time table item (3)

  18. How does RecyclerView roll? (a) | unlock reading source new posture

  19. RecyclerView how to achieve the scrolling? (2) | Fling

  20. RecyclerView Refresh list data notifyDataSetChanged() why is it expensive?