Should an experienced Android learn to be a super RecyclerView.Adapter

Time:2020-11-21

preface

After five or six years, I can’t help but sigh: Once upon a time, I couldn’t extricate myself from being addicted to the framework. No matter what needs to be done, we had to find a framework. After using it for a period of time, we found many problems and had to make do with it. Should we be controlled by others? The answer is no, or try to improve your architectural capabilities to meet more challenges in the future. The faster you wake up, the faster your progress will be. Reading the source code is painful, but more and more pain will eventually make you. If you don’t believe it, follow me to read it.

Contents of this issue

  • Comparison of common adapters
  • What is the primary pain of Adapter
  • From scratch, we write a comfortable adapter

Common adapter

name Star Open issues Last update time Packet size (AAR)
BaseRecyclerViewAdapterHelper 20.2k 162 27 days ago 81KB (V2.9.5)
baseAdapter 4.5K 107 Four years ago 10.53 KB (v3.0.3)
FlexibleAdapter 3.3K 55 15 months ago 123KB (v5.0.5)
FastAdapter 3.1K 3 Eight days ago 164KB (v5.1.0)

Through the comparison of these basic data, let you choose. How would you choose? Of course, first of all, the baseadapter and flexible adapter will be eliminated. If it is not maintained for more than a year, there will be more than 50 problems. No matter how good the framework is, it will be up to you to choose it later. No, let’s look at the problem again. At least, the most frequently updated fastadapter is fastadapter, but its package is twice as large as baserecyclerviewadapter helper. Have you done this? Do you want such a large one? If your company has a requirement for the size of the package, this is basically passed. You may think that 164KB is not big. However, if you add several more frames, the stack will be large. If you can save, you can save. It seems that the most appropriate only is the baserecyclerview adapter helper, but it has the most problems. It is too difficult and not simple at all. It’s better to make one by ourselves. By the way, we still choose to make one ourselves.

Several pain points of native adapter

  • The adapter is not universal. Every time a new business is encountered, a new adapter must be created
  • Viewholder is not universal. The problem is the same as above
  • The update of collection data can not actively let the adapter inform the page to refresh
  • Itemviewtype needs to maintain its own set of constant controls
  • With the complexity of business, onbindviewholder becomes more and more bloated

From scratch, we write a comfortable adapter

Should an experienced Android learn to be a super RecyclerView.Adapter
In the past, our implementation had to implement adapter, viewholder and model respectively, and they were seriously coupled. It was really painful to encounter a complex list.

Now we want to achieve the following goals

  • General viewholder, no longer rewriting viewholder
  • General adapter, no longer rewrite adapter
  • Only focus on the implementation of ViewModel, and realize the change of view according to the change of ViewModel, and automatically refresh the local part (not simple and crude notifydatasetchange)

General viewholder

class DefaultViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
    /**
     *Views cache
     */
    private val views: SparseArray<View> = SparseArray()
    val mContext: Context = view.context

    fun <T : View> getView(@IdRes viewId: Int): T? {
        return retrieveView(viewId)
    }

    private fun <T : View> retrieveView(@IdRes viewId: Int): T? {
        var view = views[viewId]
        if (view == null) {
            view = itemView.findViewById(viewId)
            if (view == null) return null
            views.put(viewId, view)
        }
        return view as T
    }
}

The viewholder has a single responsibility. It is responsible for holding the reference of the view and assisting you to do the right thing with the right view. One of the optimizations here is to use sparserarray cache. In fact, it has done simple optimization to prevent unnecessary loss caused by finding viewbyid again. The viewholder of baserecyclerviewadapterhelper also uses this implementation, which is generally recognized as a reliable writing method.

ViewModel abstraction

The ViewModel layer is very important. It is responsible for the binding logic of view data and which layout to load to directly view the code

public abstract class ViewModel<M, VH extends RecyclerView.ViewHolder> {

    public M model;
    public VH viewHolder;

    public abstract void onBindView(RecyclerView.Adapter<?> adapter);

    int getItemViewType() {
        return getLayoutRes();
    }

    @LayoutRes
    public abstract int getLayoutRes();

}
  • M data source abstraction, responsible for providing what kind of data
  • VH is defaultviewholder by default. Of course, there can be other extensions. There is room for extension here
  • Onbindview is responsible for the logic of binding m to VH. The purpose of returning the adapter here is to exchange data between different viewmodels in the future. Here, you can get the associated value through the adapter, and you can refresh other items through it. Isn’t it very smart.
  • As you should know, getitemviewtype is the key parameter for recyclerview to adapt to different layout layouts. The default is getlayoutres, because different layouts = different layoutres. Of course, you can extend the change logic, but at present, there is no need to change it.
  • Getlayoutres is r layout.item_ Layout to get a reference to the layout.

The biggest highlight of this design is the lack of maintenance of itemviewtype, so you can see the design of others. Here is the code of others. Maintain the itemviewtype, which is frightening. No, if there is another empty in the future_ View, do I have to extend empty_ View2, and also need to modify the logic here. The design is unscientific. You should never or try not to move the underlying logic, because if you move the underlying logic, you will face a comprehensive test.Should an experienced Android learn to be a super RecyclerView.Adapter
In my opinion, the best design is never to care about the logic of itemviewtype. The so-called header view and bottom view are just the data that you maintain at the top and bottom of the list. Finally, they are bound to the ItemView according to the sorting of the list, rather than controlled by the itemviewtype. You can have a good taste of this. Emptyview is more like a stack on the recyclerview, or you can change the list to an empty ViewModel and display it in full screen, and remove it when there is real data. In short, we operate on the removal and retention of ViewModel to keep the underlying logic of the adapter simple.

General adapter

The simplest universal adapter in history is about to appear

abstract class ListAdapter<VM : ViewModel<*, *>> : RecyclerView.Adapter<DefaultViewHolder>() {

    protected val layouts: SparseIntArray by lazy(LazyThreadSafetyMode.NONE) { SparseIntArray() }

     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DefaultViewHolder {
        return DefaultViewHolder(LayoutInflater.from(parent.context).inflate(layouts[viewType], parent, false))
    }

    override fun getItemViewType(position: Int): Int {
        val item = getItem(position)
        layouts.append(item.itemViewType, item.layoutRes)
        return item.itemViewType
    }

    override fun onBindViewHolder(holder: DefaultViewHolder, position: Int) {
        val item = getItem(position)
        item.onBindView(this)
    }

    abstract fun getItem(position: Int): VM

}

class ArrayListAdapter<M> : ListAdapter<ArrayListViewModel<M>>() {

    private val observableDataList = ObservableArrayList<ArrayListViewModel<M>>()

    init {

        observableDataList.addOnListChangedCallback(object : OnListChangedCallback<ObservableArrayList<ArrayListViewModel<M>>>() {

            override fun onChanged(sender: ObservableArrayList<ArrayListViewModel<M>>) {
                notifyDataSetChanged()
            }

            override fun onItemRangeChanged(sender: ObservableArrayList<ArrayListViewModel<M>>, positionStart: Int, itemCount: Int) {
                notifyItemRangeChanged(positionStart, itemCount)
            }

            override fun onItemRangeInserted(sender: ObservableArrayList<ArrayListViewModel<M>>, positionStart: Int, itemCount: Int) {
                notifyItemRangeInserted(positionStart, itemCount)
            }

            override fun onItemRangeMoved(sender: ObservableArrayList<ArrayListViewModel<M>>, fromPosition: Int, toPosition: Int, itemCount: Int) {
                notifyItemMoved(fromPosition, toPosition)
            }

            override fun onItemRangeRemoved(sender: ObservableArrayList<ArrayListViewModel<M>>, positionStart: Int, itemCount: Int) {
                notifyItemRangeRemoved(positionStart, itemCount)
            }
        })

    }

    override fun getItem(position: Int): ArrayListViewModel<M> {
        return observableDataList[position]
    }

    override fun getItemCount(): Int {
        return observableDataList.size
    }
    
    fun add(index: Int, element: ArrayListViewModel<M>) {
        observableDataList.add(index, element)
    }

    fun removeAt(index: Int): ArrayListViewModel<M> {
        return observableDataList.removeAt(index)
    }

    fun set(index: Int, element: ArrayListViewModel<M>): ArrayListViewModel<M> {
        return observableDataList.set(index, element)
    }
}

More than 70 lines of code, super simple.

Listadapter abstract class

  • In the implementation of layouts sparseintarray, itemviewtype is used as the key to cache layoutres. In fact, this is written to be compatible with the implementation logic of getitemviewtype of ViewModel. Of course, by default, itemviewtype is layoutres, so there is no need for caching. However, we maintain the extensibility of our framework and open this door for you to customize. Some people like to define special constants for itemviewtype. What can I do? Some people definitely refute it. You can’t customize it. It’s a good garbage. Ha ha, let it go.
  • On create viewholder many people like to pass context to the adapter and then create a layout infilter. In fact, it is not. You can use it parent.context Have you learned? Here, the layouts cache’s layoutres is used to load the corresponding view layout
  • Getitemviewtype gets the corresponding ViewModel according to the position, then gets the itemviewtype through ViewModel, and caches layoutres by the way, eh, perfect.
  • Onbindviewholder gets the corresponding ViewModel through position, and then calls back onbindview of ViewModel to trigger the model to be bound to the corresponding view. Um, perfect.
  • Getitem returns the corresponding ViewModel, and the subclass is responsible for the implementation. Because the sub class implements the cached list in different implementations, the corresponding retrieval methods may be different, so it needs to be abstracted.

ArrayListAdapter

  • The implementation of observabledatalist observablearraylist is the implementation in databinding, which is a wrapper subclass of ArrayList. If your project does not refer to databinding, please learn from me and take these three classesShould an experienced Android learn to be a super RecyclerView.Adapter

It’s shameful not to copy the class name? ):

CallbackRegistry
ListChangeRegistry
ObservableList
ObservableArrayList
  • Addonlistchangedcallback adds the monitoringonlistchangedcallback on observabledatalist, and then calls onitemrangechanged, onitemrangeinserted, onitemrangemoved and onitemrangeremoved respectively when data is refreshed. When you modify the elements of observabledatalist collection, the corresponding call back will be called back here. Is it also very simple
  • The general operations of getitem, getitemcount, add, removeat and set on observabledatalist are not explained here.
  • The arraylistviewmodel forgot to say this. Let’s take a look at the code
abstract class ArrayListViewModel<M> : ViewModel<M, DefaultViewHolder>() {
    override fun onBindView(adapter: RecyclerView.Adapter<*>?) {
        onBindAdapter(adapter = adapter as ArrayListAdapter<M>)
    }
    abstract fun onBindAdapter(adapter: ArrayListAdapter<M>)
}

This is to let the arraylistadapter object be passed to the onbindview of arraylistviewmodel, and let the corresponding ViewModel see the implementation. The following is an example. Here you can get the arraylistadapter object directly, so that you can do the operation of the corresponding adapter. Otherwise, you need to use the listadapter. You may need to force the conversion. Is that right ? The uncertainty factor has been added, so in the abstract class implementation, you should understand that the purpose of abstraction is to determine, right.

class ReportEditorViewModel : ArrayListViewModel<ReportEditorBean>(){

    override fun onBindAdapter(adapter: ArrayListAdapter<ReportEditorBean>) {

    }

    override fun getLayoutRes(): Int {
        return R.layout.item_report_editor_house
    }

}

Recyclerview extension

Due to the convenience of kotlin, we need to extend the recyclerview as follows:

fun <VM : ViewModel<*,*>> RecyclerView.bindListAdapter(listAdapter: ListAdapter<VM>,layoutManager: RecyclerView.LayoutManager? = null){
    this.layoutManager = layoutManager?: LinearLayoutManager(context)
    this.adapter = listAdapter
}

Extend the bindlistadapter to the current recyclerview, pass in our own abstract listadapter, and finally bind it together. The default configuration of layout manager is provided to reduce the generation of template code.

Page usage effect

  val adapter = ArrayListAdapter<ReportEditorBean>()

  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_report_editor)
        rv_house_list.bindListAdapter(adapter)
        adapter.add(ReportEditorViewModel())
        adapter.add(ReportEditorViewModel())
    }

An adapter, a recyclerview, and then an adapter are responsible for adding, deleting and modifying. That’s it.

Some people say what to do with click events?

It’s time to subvert your cognition again. Please forget to implement an onitemclickcallback for the extension of the adapter. It’s stupid. The answer is in our ViewModel, look at the code implementation

class ReportEditorViewModel : ArrayListViewModel<ReportEditorBean>(){

    override fun onBindAdapter(adapter: ArrayListAdapter<ReportEditorBean>) {
        viewHolder.view.setOnClickListener { 
            
        }
    }

    override fun getLayoutRes(): Int {
        return R.layout.item_report_editor_house
    }

}

In the implementation of ViewModel, can we add click events from children with viewholder? Moreover, different viewmodels can handle different click events. Do you still use onitemclickcallback to determine what the click is and how to handle it? Let’s get rid of that stupid design.

summary

I’ve implemented a super adapter for you today, OK? Feel OK, please work hard under your little hand, and give me a compliment.