preface

In project development, always cannot leave the list, when it comes to the list, there will be endless Adapter you need to implement. There are many excellent Adapter libraries. But a lot of libraries are pretty comprehensive, lots of classes, lots of features, but actually using just one or two of them is what I think a lot of people are doing. So it’s a good idea to implement one that’s easy to use, especially if you’re using lists that aren’t very complex.

The effect

Take a look at how it works, and stimulate one of your few passions to keep reading:

// Single-type list, default LinearLayoutManager
recycleView.setup<NumberInfo> {
    dataSource(initData())
    adapter {
        addItem(R.layout.layout_item) {
            bindViewHolder { data, _, _ ->
                setText(R.id.number, data? .number.toString()) } } } }// Multitype list
recycleView.setup<Any> {
    withLayoutManager { LinearLayoutManager(context) }
    dataSource(data)
    adapter {
        addItem(R.layout.item_setion_header) {
            isForViewType { data, _ - >data is SectionHeader }
            bindViewHolder { data, _, _ - >val header = data as SectionHeader
                setText(R.id.section_title, header.title)
            }
        }
        addItem(R.layout.item_user) {
            isForViewType { data, _ - >data is User }
            bindViewHolder { data, _, _ - >val user = data as User
                setText(R.id.name, user.name)
                setImageResource(R.id.avatar, user.avatarRes)
                // If your control can't find a method for assigning values, you can find it with findViewById
                val phone = findViewById<TextView>(R.id.phone)
                phone.text = user.phone
            }
        }
    }
}
Copy the code

B: well… , the feeling is ok, at least the case can be a list of code with 10 lines to complete.

Full code address

In fact, it only has 3 files. If you don’t have an address, it’s EfficientAdapter. As for how to use it, it is already described in the address, so this article is mainly about implementation ideas.

Implementation approach

The encapsulation of Adapter, in fact, is nothing more than a few callback methods to encapsulate Adapter, the most commonly used method is to define a store ViewHolder list, and then in each callback to get these ViewHolder, and then implement logic.

So which callback method is the most fucked up one? I think it’s getItemViewType. In fact, you’ll see a lot of frameworks that let you implement callback methods to get a ViewType.

Step by step, let’s start with the ViewHolder encapsulation

In the EfficientAdapter, I encapsulated the ViewHolder as BaseViewHolder:

class BaseViewHolder(parent: ViewGroup, resource: Int) : RecyclerView.ViewHolder(
        LayoutInflater.from(parent.context).inflate(resource, parent, false))Copy the code

That’s my encapsulation. Simple enough.

We need isForViewType, bindViewHolder, and other methods to implement the ViewHolder logic that we did in effect code. So I’m going to define a class that provides these methods:

abstract class ViewHolderCreator<T> {
    abstract fun isForViewType(data: T? , position:Int): Boolean
    abstract fun getResourceId(a): Int
    abstract fun onBindViewHolder(
            data: T? , items:MutableList<T>? , position:Int,  holder: ViewHolderCreator<T>)

    var itemView: View? = null

    fun registerItemView(itemView: View?). {
        this.itemView = itemView
    }

    fun <V : View> findViewById(viewId: Int): V {
        checkItemView()
        returnitemView!! .findViewById(viewId) }private fun checkItemView(a) {
        if (itemView == null) {
            throw NullPointerException("itemView is null")}}}Copy the code

In ViewHolderCreator, the getResourceId and onBindViewHolder methods are supposed to know what they’re doing, and the isForViewType method is used to determine the ViewType. Note that its return type is Boolean, This method will be discussed later. There are other methods like registerItemView and findViewById, because I want an onBindViewHolder to easily get the view.

That’s all we need to wrap the ViewHolder. The next step is to wrap the Adapter.

open class EfficientAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    var items: MutableList<T>? = mutableListOf()
    private val typeHolders: SparseArrayCompat<ViewHolderCreator<T>> = SparseArrayCompat()
}
Copy the code

Adapter first needs a generic type to represent the incoming entity class type, which defines a list of items to use as the data source. The collection of ViewHolder is stored using a SparseArrayCompat. I’m using SparseArray because I want the ViewType to be the key.

Therefore, in the onCreateViewHolder callback method, we need to take the specific ViewHolderCreator in The typeHolders according to the viewType parameter:

private fun getHolderForViewType(viewType: Int): ViewHolderCreator<T>? {
    return typeHolders.get(viewType)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    valholder = getHolderForViewType(viewType) ? :throw NullPointerException("No Holder added for ViewType $viewType")
    return BaseViewHolder(parent, holder.getResourceId())
}
Copy the code

In this way, you can obtain the corresponding ViewHolderCreator in typeHolders via the getHolderForViewType method, and then create a new ViewHolder based on the information in The ViewHolderCreator. If not, throw a null pointer exception.

In the same way, the onBindViewHolder callback method can do this:

override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
    onBindViewHolder(viewHolder, position, mutableListOf())
}

override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int, payloads:MutableList<Any>) {
    valholder = getHolderForViewType(viewHolder.itemViewType) ? :throw NullPointerException("No Holder added for ViewType "+ viewHolder.itemViewType) holder.registerItemView(viewHolder.itemView) holder.onBindViewHolder(items? .get(position), items, position, holder)
}
Copy the code

Note that there are two onBindViewHolder callback methods. The difference between them is ignored. Both implement logic here, but you can implement only one.

That leaves the getItemCount and getItemViewType callback methods. GetItemCount really doesn’t have much to say:

override fun getItemCount(a): Int= items? .size ? :0
Copy the code

Rather than implementing getItemViewType, let’s talk about how to add data to typeHolders:

fun register(holder: ViewHolderCreator<T>) = apply {
    var viewType: Int = typeHolders.size()
    while (typeHolders.get(viewType) ! =null) {
        viewType++
    }
    typeHolders.put(viewType, holder)
}
Copy the code

The type of typeHolders is SparseArrayCompat. Here I use ViewType as the key. In the register method, you can see that every time you register the typeHolders, the ViewType automatically increases by one (because the length of the typeHolders gets longer). To achieve the effect of not repeating, when the time to achieve getItemViewType, you can directly take out. The interference of specific business is avoided.

Finally, look at the implementation of getItemViewType:

override fun getItemViewType(position: Int): Int {
    if (items == null) {
        throw NullPointerException("adapter data source is null")}for (i in 0 until typeHolders.size()) {
        val holder = typeHolders.valueAt(i)
        val data= items? .getOrNull(position)if (holder.isForViewType(data, position)) {
            return typeHolders.keyAt(i)
        }
    }

    // No matching viewType was found
    throw NullPointerException(
            "No holder added that matches at position=$position in data source")}Copy the code

The idea of this method is to iterate through The typeHolders and judge whether the conditions are met through the isForViewType method of ViewHolderCreator. If yes, then take out the viewType from the typeHolders and return it.

Since the viewType in typeHolders is incrementing, the return value of getItemViewType would be 0,1,2,3…

How is isForViewType implemented in practice?

For example, if your data source consists of multiple entity classes, such as:

private List<Object> data = new ArrayList<>();
data.add(new User("Marry".17, R.drawable.icon2, "123456789XX"));
data.add(new SectionHeader("My Images"));
data.add(new Image(R.drawable.cover1));
Copy the code

So in building the EfficientAdapter, the generic type is naturally passed in Object, and then in the isForViewType method you can differentiate between types like this:

 // represents the User type
 public boolean isForViewType(Object data, int position) {
   return data instanceof User;
}

 // This is the SectionHeader type
 public boolean isForViewType(Object data, int position) {
   return data instanceof SectionHeader;
}

 // represents the type Image
 public boolean isForViewType(Object data, int position) {
   return data instanceof Image;
}
Copy the code

If your data source has only one entity class, but there are certain fields in the entity class that distinguish types, you can do this:

 // represents the User type
 public boolean isForViewType(ListInfo data, int position) {
   return data.type = ListInfo.USER
}

 // This is the SectionHeader type
 public boolean isForViewType(ListInfo data, int position) {
   return data.type = ListInfo.HEADER
}

 // represents the type Image
 public boolean isForViewType(ListInfo data, int position) {
   return data.type = ListInfo.IMAGE
}
Copy the code

Other circumstances may be determined according to specific circumstances.

At this point, Adapter encapsulation has been completed, then you can define some data sources to add, delete, check and change methods, such as:

/ / bind RecyclerView
fun attach(recyclerView: RecyclerView) = apply { recyclerView.adapter = this }

// Submit data
fun submitList(list: MutableList<T>) {
    this.items? .clear()this.items? .addAll(list) notifyDataSetChanged() }Copy the code

At this point, it can be used simply and roughly:

adapter = EfficientAdapter<SectionHeader>()
        .register(object : ViewHolderCreator<SectionHeader>() {
            override fun isForViewType(data: SectionHeader? , position:Int) = data! =null
            override fun getResourceId(a) = R.layout.item_setion_header

            override fun onBindViewHolder(
                    data: SectionHeader? , items:MutableList<SectionHeader>? , position:Int,
                    holder: ViewHolderCreator<SectionHeader>) {
                setText(R.id.section_title, data.title) } }).attach(recycle_view) adapter? .submitList(data)
Copy the code

But it’s a little different from the use. So, here’s where Kotlin comes in.

Extension functions and DSLS

I believe that those of you who have studied Kotlin know these two things, and they can provide more possibilities for our code.

ViewHolderCreator DSL

Since ViewHolderCreator is an abstract class, wrapping it in a DSL requires a default implementation class (maybe directly, but that’s the only way I can think of) :

class ViewHolderDsl<T>(private val resourceId: Int) : ViewHolderCreator<T>() {
    private var viewType: ((data: T? , position:Int) - >Boolean)? = null
    private var viewHolder: ((data: T? , position:Int, holder: ViewHolderCreator<T>) -> Unit)? = null

    fun isForViewType(viewType: (data: T? , position:Int) -> Boolean) {
        this.viewType = viewType
    }

    fun bindViewHolder(holder: (data: T? , position:Int, holder: ViewHolderCreator<T>) -> Unit) {
        viewHolder = holder
    }

    override fun isForViewType(data: T? , position:Int): Boolean {
        returnviewType? .invoke(data) ?: (data! =null)}override fun getResourceId(a) = resourceId

    override fun onBindViewHolder(
            data: T? , items:MutableList<T>? , position:Int, holder: ViewHolderCreator<T>,
            payloads: MutableList<Any>){ viewHolder? .invoke(data, position, holder)
    }
}
Copy the code

The code is relatively clear, which is the realization of three abstract methods. Because getResourceId is simple, you just pass the value in the constructor.

To implement the ViewHolderDsl, let’s define an extension function for the EfficientAdapter and use it to call the register method:

fun <T : Any> EfficientAdapter<T>.addItem(resourceId: Int, init: ViewHolderDsl<T>. () -> Unit) {
    val holder = ViewHolderDsl<T>(resourceId)
    holder.init()
    register(holder)
}
Copy the code

Simply create a ViewHolderDsl and call the register method.

If you want to create a new Adapter, you can use a new Adapter.

fun <T : Any> efficientAdapter(init: EfficientAdapter<T>. () -> Unit): EfficientAdapter<T> {
    val adapter = EfficientAdapter<T>()
    adapter.init()
    return adapter
}
Copy the code

So the crude sample code above would look something like this:

adapter = efficientAdapter<Any> { addItem(R.layout.item_setion_header) { isForViewType { it ! =null }
        bindViewHolder { data, _, _ ->
            setText(R.id.section_title, data.title) } } }.attach(recycle_view) adapter? .submitList(data)
Copy the code

The code is much cleaner and simpler. Because in the ViewHolderDsl, the default implementation of isForViewType is Data! =null, so if it is a single-type list, this method can be left blank.

The code is much simpler, but there are always adapter objects that need to be defined and bound to the RecycleView, so the more elegant way is to define an extension function for the RecycleView to wrap it all up.

First of all, we implement a class called RecycleSetup, in this class, the RecycleView configuration and Adapter operation, data operation and so on all packaged:

class RecycleSetup<T> internal constructor(private val recyclerView: RecyclerView) {

    var items = mutableListOf<T>()
    var adapter: EfficientAdapter<T>? = null
    var context = recyclerView.context

    fun dataSource(items: MutableList<T>) {
        this.items.clear()
        this.items = items
    }

    fun withLayoutManager(init: RecycleSetup<T>. () -> RecyclerView.LayoutManager) =
            apply { recyclerView.layoutManager = init() }

    fun adapter(init: EfficientAdapter<T>. () -> Unit) {
        this.adapter = EfficientAdapter() init.invoke(adapter!!) recyclerView.adapter = adapter adapter? .submitList(this.items)
    }

    fun submitList(list: MutableList<T>) {
        this.items.clear()
        this.items = list adapter? .submitList(this.items)
    }

    fun getItem(position: Int): T = items[position]
}
Copy the code

Code is simple, I believe everyone can understand.

With this class, we can finally implement the extension function for the RecycleView:

fun <T> RecyclerView.setup(block: RecycleSetup<T>. () -> Unit): RecycleSetup<T> {
    val setup = RecycleSetup<T>(this).apply(block)
    if (layoutManager == null) {
        layoutManager = LinearLayoutManager(context)
    }
    return setup
}

fun <T> RecyclerView.submitList(items: MutableList<T>) {
    if(adapter ! =null && adapter is EfficientAdapter<*>) {
        (adapter as EfficientAdapter<T>).submitList(items)
    }
}
Copy the code

If layoutManager is empty the LinearLayoutManager is implemented by default. Finally, the rough code above can be written to the same effect as the first one:

recycleView.setup<SectionHeader> {
    adapter {
        addItem(R.layout.item_setion_header) {
            bindViewHolder { data, _, _ ->
                setText(R.id.section_title, data.title)
            }
        }
    }
}
recycleView.submitList(data)
Copy the code

The full code and EfficientAdapter EfficientAdapter examples are available here.

conclusion

The interesting thing about this article is that the viewType is self-incrementing. In this case, the user only needs to implement isForViewType, which prevents your entity class from having to inherit from a Base class.

Of course, this is relatively simple compared to the various big names’ libraries, so the reason for writing this article is to share one of my thoughts on encapsulating code, step by step, from zero to something. I believe that many people need this kind of things, more than moving bricks all day interesting, will also learn a little knowledge.

Thank you for reading the whole article.