Preface
Five or six years in a flash, time is running out, can't help exclamating: once geometrically, when we were obsessed with the framework, we had to find a framework for whatever we need. After a while, we found many problems, and sometimes we had to use it. Should we be left and right by others?The answer is No, or try to improve your architecture to meet more of the challenges ahead.The faster you wake up, the faster you will progress. Reading the source is painful, but more and more pains will eventually make you. Don't believe you follow me.
Contents of this issue
- Comparison of Common Adapter s
- Where is the pain point of the native Adapter
- Start from scratch and write our own comfortable Adapter
Common Adapter s
Name Star Unsolved problem Last Update Time Package 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 8 days ago 164KB (v5.1.0)By comparing these basic data and letting you choose, what would you choose?Of course, the baseAdapter and FlexibleAdapter will be excluded first. They are not maintained for more than a year. There are more than 50 questions. The framework is better if you choose it later. Let's see FastAdapter is the one with the least and most frequent updates to the questions, but it is twice as large as BaseRecyclerViewAdapter Help. Have you done so much?If your company has requirements for package size, this is basically pass, you may think 164KB is not big, but if there are more frames, it will be big to overlay, which will save you time.It looks like the best fit is the BaseRecyclerViewAdapterHelper, but it has the most problems, it's too difficult, it's not simple at all.It's better to do it by ourselves. By the way, we still choose to do it by ourselves.
Several Pain Points for Native Adapter
- Adapter is not generic, create a new Adapter whenever you encounter a new business
- ViewHolder is also not generic, the same problem
- Updates to collection data do not actively refresh the Adapter notification page
- ItemViewType needs to maintain its own set of constant controls
- onBindViewHolder gets bloated with the complexity of the business
Start from scratch and write our own comfortable Adapter
Previously, our implementations were adapters, ViewHolder s, Model s, and they were heavily coupled, making it incredibly painful to encounter a complex list.
Now we want to achieve the following goals
- Generic ViewHolder, no longer rewrite ViewHolder
- Generic Adapter, no longer rewrite the Adapter
- Focus solely on implementing ViewModel and enabling View to change depending on the changes in ViewModel and automatically do a local refresh (not a simple, rough NuotifyDataSetChange)
Universal 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 } }
ViewHolder has a single responsibility to hold a reference to the View and help you do the right thing with the right View. One of the optimizations here is to use the SparseArray cache, which is actually to make simple optimizations to prevent unnecessary wastage from findViewById again.The ViewHolder for BaseRecyclerViewAdapterHelper also uses this implementation, which is generally accepted as a more reliable way to write.
ViewModel abstraction
This layer of ViewModel is critical because it is responsible for the binding logic of the View data and which Layout to load to see the code directly
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 The abstraction of the data source, responsible for providing what kind of data
- VH defaults to DefaultViewHolder, but there are other extensions, which leave room for extensions
- onBindView is responsible for the logic of binding M to VH. It is not wise to return the Adapter here so that later different ViewModel s can interact with data, so that you can get the associated value through the Adapter and refresh other Item s through it.
- As you should know, getItemViewType is a key parameter for RecyclerView to adapt different layouts of Layout. The default is getLayoutRes, because different layouts = different LayoutRes, and of course you can also extend the change logic, but for now there is no need to change it.
- getLayoutRes is also known as R.layout.item_layout, get a reference to the layout.
The biggest highlight of this design is that it lacks the maintenance of ItemViewType, which lets you see others'designs. Here's their code. Maintain ItemViewType, which is scary. If you have another EMPTY_in the futureVIEW, do I have to extend an EMPTY_VIEW2, but also to modify the logic here, so the design is not scientific, you should always or try not to move the underlying logic, because you move the underlying logic to face the full test.
In my opinion, the best design is to never care about the logic of the ItemViewType, and the so-called HeadView and Bottom View are just the data you maintain at the top and bottom of the List, which is ultimately bound to the ItemView based on the order of the List, not controlled by the ItemViewType, which you'll savor.EmptyView is more like a stack pressed on RecyclerView, or you can change a List into an Empty ViewModel and remove it when there is real data on the RecyclerView. In short, what we do is to remove and retain the ViewModel and keep the underlying logic of the Adapter simple.
General Adapter
The simplest generic Adapter in history is about to appear, clap your friends
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) } }
Over 70 lines of code, super simple.
ListAdapter abstract class
- The implementation of layouts SparseIntArray caches layoutRes with itemViewType as the Key. This is written to be compatible with the implementation logic of the getItemViewType that extends ViewModel. Of course, by default, itemViewType is layoutRes, so you can also avoid caching, but we keep our framework extensible and open this door for you to customize.Some people like to define special constants for itemViewType. What can I do about it? Some people definitely retort that you can't customize it. Well designed garbage, go with him.
- Many people like to pass Context in to Adapter and create LayoutInflater, but otherwise, you can use itParent.contextDid you learn?This uses layoutRes from the layouts cache to load the corresponding View layout
- getItemViewType gets the corresponding ViewModel based on position, gets itemViewType through ViewModel, and then layoutRes in the cache by the way, well, perfect.
- The onBindViewHolder takes the corresponding ViewModel through position, calls back the onBindView of the ViewModel, triggers the Model to bind to the corresponding View, well, perfect.
- getItem returns the corresponding ViewModel, and the subclass is responsible for the implementation, because the subclass implements a cached List with different implementations, the corresponding fetches may be different and need to be abstracted.
ArrayListAdapter
- The implementation of observableDataList ObservableArrayList is an implementation in Databindings, a wrapper subclass of ArrayList. If your project does not reference Databinding, learn from me, take these three classes and you're ok ay
It's shameful for a map not to copy a class name (I didn't do that, what about you?):
CallbackRegistry ListChangeRegistry ObservableList ObservableArrayList
- addOnListChangedCallback adds the OnListChangedCallback that monitors the observableDataList, and then calls onItemRangeChanged, onItemRangeInserted, onItemRangeMoved, onItemRangeRemoved when the data refreshes, respectively. When you modify the elements of the observableDataList collection, the corresponding callback comes here, is it also simple or not
- The general operations of getItem, getItemCount, add, removeAt, set on observableDataList are not explained much here.
- ArrayListViewModel forgot to say that, let's look at the code first
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 pass to the onBindView of the ArrayListViewModel. Let the corresponding ViewModel see the implementation. Here's an example. You can get the ArrayListAdapter object directly here, so you can do the operation of the corresponding Adapter. Otherwise, you may need to force the ListAdapter when you use it, but are you forcing the right one?What?Adding uncertainty, so here in the abstract class implementation, you have to understand that abstraction is for certainty, 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 also need to extend RecyclerView, such as code:
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 together.It also provides a default configuration for layoutManager to reduce template code generation.
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 the Adapter is responsible for adding, deleting, and altering.It's that simple.
Some people say what to do with click events?
It's time to overturn your perception. It's foolish to forget to implement an onItemClickCallBack for the Adapter extension.The answer is in our ViewModel, see 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 child to child with viewHolder?Click event handling can also be different for different ViewModels. Do you still use onItemClickCallBack to determine what clicks do?Throw away that silly design.
summary
You've got a super Adapter today, okay?Feel Ok, hard work on your little hand, order a compliment oh dear.