preface
Flash five or six years, time wasted, can not help but sigh: once upon a time, addicted to the framework can not extricate themselves, no matter what needs to find a framework out, and then used for a period of time, found a lot of problems, a lot of times have to do with, we should be left and right by others? The answer is No, but 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 source code is painful, but more and more pain will eventually achieve you, do not believe you follow me to see.
The current content
- Comparison of Common Adapter
- Where is the pain point of native Adapter
- From scratch, we wrote a comfortable Adapter
Commonly used Adapter
The name | Star | Unresolved problem | Last update time | Packet size (aar) |
---|---|---|---|---|
BaseRecyclerViewAdapterHelper | 20.2 k. | 162 | 27 days ago | 81 KB (V2.9.5) |
baseAdapter | 4.5 K. | 107 | Four years ago | 10.53 KB (v3.0.3) |
FlexibleAdapter | 3.3 K. | 55 | 15 months ago | 123 KB (v5.0.5) |
FastAdapter | 3.1 K. | 3 | Eight days ago, | 164 KB (v5.1.0) |
Through the comparison of these basic data, let you choose, what would you choose? Of course, first of all, we will remove baseAdapter and FlexibleAdapter. If you do not maintain it for more than a year, there will be more than 50 problems. No matter how good the framework base is, you have to rely on yourself after choosing. Can it package for big BaseRecyclerViewAdapterHelper doubled, excuse me what did you do the authors ha, want so much? If your company has requirements on the size of the package, this package has been basically passed. You may think 164KB is not large, but if there are several more frames, they will be added up to a larger size. Only BaseRecyclerViewAdapterHelper seems to be the most appropriate, but its problem is the most, it is too difficult, is not simple. Why don’t we do it ourselves? By the way, let’s choose to do it ourselves.
The original raw Adapter several pain points
- Adapters are not universal. You need to create new adapters every time you encounter new services
- ViewHolder is not universal. Same question
- The Adapter cannot notify the page to refresh when the collection data is updated
- ItemViewType needs to maintain its own set of constant controls
- With the complexity of the business, onBindViewHolder becomes more and more bloated
From scratch, we wrote a comfortable Adapter
In our previous implementation, we had to implement Adapter, ViewHolder, and Model separately, and they were heavily coupled, so it was a pain to have a complex list.
Now we need to achieve the following goals
- Generic ViewHolder, no longer rewriting the ViewHolder
- Generic Adapter, no longer override Adapter
- Just focus on implementing the ViewModel and implementing the View as it changes according to the ViewModel, automatically doing local refresh (not just a rude NotifyDataSetChange)
Gm’s 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 } }Copy the code
The ViewHolder is responsible for holding a reference to the View and helping you do the right thing with the right View. One optimization is to use the SparseArray cache. This is a simple optimization to prevent unnecessary waste from finding ViewByID again. BaseRecyclerViewAdapterHelper ViewHolder is also in this implementation, it is generally accepted that the spectrum of the writing.
The ViewModel abstract
The ViewModel layer is the key. 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(); }Copy the code
- M data source abstraction, responsible for providing what kind of data
- The VH defaults to DefaultViewHolder, but there can be other extensions as well, with room for extensions
- OnBindView is responsible for binding M to VH logic, here the Adapter is sent back in case of data interaction between different ViewModels, here you can get the associated value through the Adapter, and you can use it to refresh other items, isn’t it clever?
- GetLayoutRes is the default parameter for RecyclerView and RecyclerView. You can use getLayoutRes to change the Layout, but you don’t need to change it.
- GetLayoutRes (r.layout.Item_layout) gets a reference to the layout.
If there is an EMPTY_VIEW, then I need to extend the EMPTY_VIEW2 item. If there is an EMPTY_VIEW, then I need to extend the EMPTY_VIEW2 item. And you need to modify the logic here. This is not a scientific design. You should never or try not to change the underlying logic, because you should change 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 head View and bottom View are just the data you maintain at the top and bottom of the List, and eventually bind to the ItemView according to the order of the List, not controlled by the ItemViewType. You savor that. And EmptyView is more like a pressure on the RecyclerView stack, or you change the List into an Empty ViewModel and RecyclerView full-screen display, when there is real data to remove it, in short, we operate is the ViewModel to and stay, Keep the underlying Adapter logic concise.
Universal Adapter
The simplest universal Adapter in history is about to appear. Clap your hands
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)
}
}
Copy the code
It’s over 70 lines of code. It’s super simple.
ListAdapter abstract class
-
The layouts SparseIntArray implementation uses itemViewType as the Key to cache layoutRes. The layouts SparseIntArray implementation uses itemViewType as the Key to cache layoutRes. Of course by default itemViewType is layoutRes, so you can do without caching, but we’ll keep our framework extensible and open the door for you to customize. Some people like to define special constants for itemViewType, what can I do? Some people will say, you can’t customize this, nice design rubbish, haha, whatever.
-
OnCreateViewHolder A lot of people like to pass a Context to an Adapter and then create a LayoutInflater, but you can use parent-Context.context, okay? The layouts cache’s layoutRes are used to load the corresponding View layout
-
GetItemViewType gets the corresponding ViewModel based on the position, and then it gets the itemViewType from the ViewModel, and then it caches the layoutRes. Yeah, perfect.
-
OnBindViewHolder gets the corresponding ViewModel from position, then calls back to the ViewModel’s onBindView, triggering the Model to bind to the corresponding View. Perfect.
-
GetItem returns the corresponding ViewModel, and the subclass is responsible for implementing it. Because the subclass implements the cached List in different implementations, the corresponding fetching method may be different, so it needs to be abstracted.
ArrayListAdapter
- The implementation of observableDataList ObservableArrayList, which is the implementation of Databinding, is a wrapper subclass of ArrayList. If your project does not reference Databinding, then please do what I did and just bring these three classes
Shame on tile for not copying class name (I didn’t do that, did you?) :
CallbackRegistry
ListChangeRegistry
ObservableList
ObservableArrayList
Copy the code
-
Add the OnListChangedCallback that listens to the observableDataList, Then call onItemRangeChanged, onItemRangeInserted, onItemRangeMoved, and onItemRangeRemoved on data refresh. When you modify an element in the observableDataList collection, It’s going to call back here, and that’s easy
-
The general operations of getItem, getItemCount, add, removeAt, and set on the observableDataList are not explained here.
-
ArrayListViewModel forgot to say that, so let’s 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>)
}
Copy the code
This is for the purpose of passing the ArrayListAdapter object to the onBindView of the ArrayListViewModel, and the corresponding ViewModel, so let’s see if we can implement it, so here’s an example, where we can get the ArrayListAdapter object directly, If you don’t use the ListAdapter, you might want to use the ListAdapter. If you don’t use the ListAdapter, you might want to use the ListAdapter. It adds uncertainty, so it’s implemented in an abstract class, and you know, the purpose of abstraction is to be deterministic, right.
class ReportEditorViewModel : ArrayListViewModel<ReportEditorBean>(){
override fun onBindAdapter(adapter: ArrayListAdapter<ReportEditorBean>) {
}
override fun getLayoutRes(): Int {
return R.layout.item_report_editor_house
}
}
Copy the code
RecyclerView extension
Thanks to The convenience of Kotlin, we also need to extend the RecyclerView, such as code:
fun <VM : ViewModel<*,*>> RecyclerView.bindListAdapter(listAdapter: ListAdapter<VM>,layoutManager: RecyclerView.LayoutManager? = null){ this.layoutManager = layoutManager? : LinearLayoutManager(context) this.adapter = listAdapter }Copy the code
Extend bindListAdapter to the current RecyclerView and pass in our own abstract ListAdapter, finally binding together. It also provides a default configuration of layoutManager to reduce template code generation.
Page Usage
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())
}
Copy the code
A Adapter, a RecyclerView, and then Adapter is responsible for adding and deleting. It’s that simple.
Someone said what about click events?
It’s time to change your perception, and forget about implementing an onItemClickCallBack for the Extension to the Adapter. The answer is in our ViewModel, looking 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
}
}
Copy the code
In the ViewModel implementation, shouldn’t a viewHolder be used to add click events to the child? Click events can also be handled differently for different viewModels. Do you still use onItemClickCallBack to determine what a click is? Get rid of that stupid design.
conclusion
Today took you to implement a super Adapter, ok? Feel Ok, trouble hard under your little hands, point a like oh kiss.
Open source address
Github RecyclerViewAdapter
developers
- I the principal
- Jetpack.net.cn
- Jane’s book
- The Denver nuggets