Moment For Technology

A brand new RecyclerView Adapter framework source source

Posted on Dec. 2, 2022, 4:33 p.m. by 王志偉
Category: android Tag: kotlin

RecyclerViewAdapter

Redefinition of RecyclerView Adapter packaging, the pursuit of both simple and practical, combined with the advanced features of Kotlin, optimize the code writing, really achieve high cohesion and low coupling

Open source address

Github RecyclerViewAdapter

Framework design core ideas

  • Instead of notifyDataSetChanged mindless operation, use ObservableList to automatically match data and implement local refresh
  • The truly generic ViewHolder abstraction focuses only on the Layout XML Layout from here on out
  • Real generic Adapter, no longer write Adapter subclass
  • ItemViewType automatically matches the object Layout XML without caring about its details
  • Scientific subcontracting, really do the framework of each need (general list just reference Adapter-core core library)
  • Extend the version of Anko Layout, experience the charm of Anko Layout, and can obtain at least 3 times higher than the XML loading efficiency
  • Like a stack, reuse every module of the page, and say goodbye to fragments

The design

planning

  • Core library Core improvement
  • Anko extension
  • FlexboxLayout extension
  • SortedList extension
  • Kotlin DSL extension support
  • The paging 3 extension
  • DiffUtil extension
  • DataBinding extensions
  • And so on.. There are good ideas in the future to continue to expand

Now we have an ArrayListAdapter, a SortedListAdapter, and a PagingAdapter.

  • First, in principle, I would never design a super large Adapter to support various functions. A single responsibility should be used throughout
  • Second, keep the characteristics of each Lib, can be selected according to the different business, the maximum extent of the reduction of package volume, really reflects no best and most complete, only the most appropriate idea
  • Third, keep the feature of dynamic extension, you understand the principle, you can also customize according to their own needs, the future official MergeAdapter combination, and we do a WrapAdapter in the future, you will find more combinations of possibilities.
  • Fourthly, due to the reuse of ViewHolder, there will always be some problems. At present, this framework can perfectly solve the problem of UI duplication caused by reuse. Keep good reuse and keep the page refresh right.

What's missing?

Do you think there's something missing? On the blank layout, loading, the drop-down load, drag, head of layout, layout, folding, divider, animation and so on, these later will be with you to achieve, the above planning prefer to encapsulation of the underlying framework, and these functions more business component, in a different direction, don't try so hard oh, take your perfect step by step, to experience the fun in encapsulation Adapter

Size of the library

The name release aar size other
Core 35kb The core library currently contains an implementation of ArrayListAdapter, the most basic and useful extension
Anko 9kb The same is ArrayListAdapter, because of a high degree of abstraction, so currently eliminate AnkoListAdapter, with ArrayListAdapter instead
Sorted 11kb SortedListAdapter extension implementation
Paging 14kb PagingListAdapter Extension adaptation

What are the advantages of each Adapter and how to choose it?

The name advantage disadvantage What to do
ArrayListAdapter Simple and practical, easy to extend, ViewHolder reuse rate is high, DSL support elegant writing Processing troublesome lists that need to be sorted has low performance A general list without regard to sorting
SortedListAdapter Sorting is super easy High intrusive, need Model layer inheritance implementation, currently optimized as an interface, increased the scope of use Any list that needs to be sorted
PagingListAdapter Built-in loading status, after the completion of background computing notification refresh, high loading efficiency It is highly invasive, requires the Model layer to inherit the implementation, has high learning cost, and is difficult to master A list suitable for automatic loading of pages

Environment requires

  • Kotlin
  • JAVA
  • AndroidX sorry, currently according to the latest AndroidX adaptation, if you need other private chat with me.

How to use

ArrayListAdapter

step1

Create the XML layout as before

? xml version="1.0" encoding="utf-8"?  androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="5dp"
    android:layout_margin="5dp"

    LinearLayout
        android:background="? attr/selectableItemBackground"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="5dp"
        android:orientation="vertical"

        TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/colorPrimary"
            android:textSize="22sp" /

        TextView
            android:id="@+id/tv_subTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/colorAccent"
            android:textSize="18sp" /

    /LinearLayout

/androidx.cardview.widget.CardView
Copy the code

step2

Define the ViewModel and the Model, you can see that the logic is simple and clear, refresh yourself only need to update the Model, and then update the ViewModel through the adapter, refresh others, also need to update through the adapter, For complex pages, just create a new subclass of ArrayItemViewModel and create a new XML layout. As you can see from the code here, the same ViewModel can reuse many XML layouts in the future. Fully achieve ViewModel, View, Model three roles of arbitrary reuse. Provide the lowest level of support for business diversification.

/** * Model */ data class ModelTest(var title: String, var subTitle: String) class ArrayViewModelTest : ArrayItemViewModelModelTest, DefaultViewHolder() { var index = 0 override fun onBindView(adapter: ArrayListAdapter?) { getViewTextView(R.id.tv_title)? .text = model.title getViewTextView(R.id.tv_subTitle)? .text = model.subTitle } override fun getLayoutRes() = R.layout.item_test override fun getViewHolder(view: View): DefaultViewHolder {return DefaultViewHolder(view).apply {
            itemView.setOnClickListener {
                val item = adapter.get(adapterPosition) as ArrayViewModelTest
                item.model.title = "${index++}"
                adapter.set(adapterPosition, item)
            }
        }
    }

}
Copy the code

So far, we've optimized this, and we did find a problem with that, in the findViewById cache, we do have multiple calls, and we do have multiple calls to the onClick listener, because the onBindView is going to call back depending on the lifecycle of the ItemView, so to optimize this, I've moved the creation of a ViewHolder down to the business layer. All I need to do is set the listener in the ViewHolder and find the View.

The reuse logic is as follows:

step3

Activity to add, delete and change, add, delete and change are on the ViewModel layer operation, simple and practical.

/**
 * Activity
 */
class ArrayListActivity : AppCompatActivity() {

    private val mArrayListAdapter by lazy {
        ArrayListAdapter()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_array_list) rv_list.bindListAdapter(mArrayListAdapter) // Add a new_add.settext ("New").setOnClickListener {
            mArrayListAdapter.add(ArrayViewModelTest().apply {
                model = ModelTest("Title"."Subtitle")})} // Delete the first delete.settext ()"Delete").setOnClickListener {
            if (mArrayListAdapter.size  0)
                mArrayListAdapter.removeAt(0)
            else
                toast("Please add a new use case and try again"} var updateSize = 0 update.settext ();"Update").setOnClickListener {
            updateSize++
            if (mArrayListAdapter.size  0) {
                val randomInt = Random.nextInt(0, mArrayListAdapter.size)
                mArrayListAdapter.set(randomInt, ArrayViewModelTest().apply {
                    model = ModelTest("Title$updateSize"."Subtitle$updateSize")})}else {
                toast("Please add a new use case and try again")}}}}Copy the code

AnkoListAdapter

step1

Define AnkoLayout, which makes more sense than the previous method. On the one hand, you no longer need to cache the View through the Map. On the other hand, the click time can be called directly to the user, avoiding the problem of repeating find or binding onClick.

/**
 * AnkoItemView
 */
class AnkoItemView(val itemClick: () - Unit) : AnkoComponentViewGroup {

    var tvTitle: TextView? = null
    var tvSubTitle: TextView? = null
    var view: View? = null

    @SuppressLint("ResourceType")
    override fun createView(ui: AnkoContextViewGroup) = with(ui) {

        cardView {

            layoutParams = FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.WRAP_CONTENT
            ).apply {
                margin = dip(5)
            }

            verticalLayout {

                setOnClickListener {
                    itemClick()
                }

                val typedValue = TypedValue()
                context.theme
                    .resolveAttribute(android.R.attr.selectableItemBackground, typedValue, true)
                val attribute = intArrayOf(android.R.attr.selectableItemBackground)
                val typedArray =
                    context.theme.obtainStyledAttributes(typedValue.resourceId, attribute)

                background = typedArray.getDrawable(0)

                layoutParams = FrameLayout.LayoutParams(
                    FrameLayout.LayoutParams.MATCH_PARENT,
                    FrameLayout.LayoutParams.WRAP_CONTENT
                ).apply {
                    padding = dip(10)
                }

                tvTitle = textView {
                    textSize = px2dip(60)
                    textColorResource = R.color.colorPrimary
                }.lparams(matchParent, wrapContent)

                tvSubTitle = textView {
                    textSize = px2dip(45)
                    textColorResource = R.color.colorAccent
                }.lparams(matchParent, wrapContent)

            }
        }

    }
}

Copy the code

step2

So let's define ViewModel, Model, and there's a little bit of detail here, in the ArrayListAdapter example I'm setting the click event in the onBindView, and the downside of that is that every time you redo the onBindView it's going to set the click event, which is really bad, So in the Anko version, I optimized it so that onCreateView handles the click event, so I did it once.

/** * Model */ data class ModelTest(var title: String, var subTitle: String) /** * ViewModel */ class AnkoViewModelTest : AnkoItemViewModelModelTest, AnkoItemView() { var index = 0 override fun onBindView(adapter: AnkoListAdapter) { ankoView.tvTitle? .text = model.title ankoView.tvSubTitle? .text = model.subTitle } override fun onCreateView(): AnkoItemView {return AnkoItemView{
            model.title = "${index++}"
            reBindView()
        }
    }

}
Copy the code

step3

Add, delete, and modify the Activity

/**
 * Activity
 */
class AnkoLayoutActivity : AppCompatActivity() { private val mAnkoListAdapter by lazy { AnkoListAdapter() } override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) AnkoLayoutComponent(mAnkoListAdapter).setContentView(this).apply {// Add one new_add.setText("New").setOnClickListener {
                mAnkoListAdapter.add(AnkoViewModelTest().apply {
                    model = ModelTest("Title"."Subtitle")})} // Delete the first delete.settext ()"Delete").setOnClickListener {
                if (mAnkoListAdapter.size  0)
                    mAnkoListAdapter.removeAt(0)
                else
                    toast("Please add a new use case and try again"} var updateSize = 0 update.settext ();"Update").setOnClickListener {
                updateSize++
                if (mAnkoListAdapter.size  0) {
                    val randomInt = Random.nextInt(0, mAnkoListAdapter.size)
                    mAnkoListAdapter.set(randomInt, mAnkoListAdapter.getItem(randomInt).apply {
                        model.also {
                            it as ModelTest
                            it.title = "$updateSize"}})}else {
                    toast("Please add a new use case and try again")
                }
            }

        }
    }

}

/**
 * View
 *
 */
class AnkoLayoutComponent(private val ankoListAdapter: AnkoListAdapter) : AnkoComponentAnkoLayoutActivity {

    override fun createView(ui: AnkoContextAnkoLayoutActivity) = with(ui) {

        verticalLayout {

            recyclerView {
                bindListAdapter(ankoListAdapter)}.lParams (matchParent) {weight = 1F} // Anko compatible XML layout loading includeView(R.layout.include_button_bottom) } } }Copy the code

SortedListAdapter

SortedListAdapter and the above two use the same data structure, the algorithm is different, because the SortedList itself to keep the order, so the use of binary search algorithm, to improve the efficiency of search, but it has a shortcoming is more intrusive, You need to implement the SortedModel abstract class in the Model layer, because it cannot sort without SortId and cannot be unique without UniqueId. Of course, you can treat the two fields as one, but my experience tells me that it makes more sense to separate them. It needs to be understood properly, but it has its advantages, and sometimes it saves a lot of work by automatically sorting. Let's go straight to the examples

step1

Use the layout code from ArrayListAdapter to go to step 2

step2

To define a Model, we need to inherit from SortedModel, and then implement SortedItemViewModel, using the same method as ArrayListAdapter. Right

SortedModelTest(val sortedId, RecyclerView, RecyclerView, RecyclerView, RecyclerView) */ data class SortedModelTest(val sortedId, RecyclerView, RecyclerView) Int, var title: String, var subTitle: String) : SortedModel(sortedId, title) class SortedItemViewModelTest : SortedItemViewModelSortedModelTest,DefaultViewHolder() { var index = 0 override fun onBindView(adapter: SortedListAdapter) { viewHolder.getViewTextView(R.id.tv_title)? .text = model.title viewHolder.getViewTextView(R.id.tv_subTitle)? .text = model.subTitle } override fun getLayoutRes() = R.layout.item_test override fun getViewHolder(view: View): DefaultViewHolder {return DefaultViewHolder(view).apply {
            itemView.setOnClickListener {
                val item = adapter.getItem(adapterPosition) as SortedItemViewModelTest
                item.model.subTitle = "Refresh yourself"${index++}"
                adapter.updateItem(adapterPosition,item)
            }
        }
    }

}
Copy the code

step3

They've been implemented, added, deleted, changed, and there's a logic that needs to be made clear

  • When the Adapter add function is used, the Item will be updated only if the sortId and uniqueId are the same. Otherwise, the Item will be added
  • When using Adapter updateItem, you only need the uniqueId to trigger the update
  • UpdateItem, with the same uniqueId but different sortId, will trigger an update and reorder

So the use of the need to pay attention to the appropriate place to use the appropriate method

class SortedActivity : AppCompatActivity() {

    private val mSortedListAdapter by lazy {
        SortedListAdapter()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_array_list) rv_list.bindListAdapter(mSortedListAdapter) (0.. 10).map { mSortedListAdapter.add(SortedItemViewModelTest().apply { model = SortedModelTest(it,"Title$it"."Subtitle$it")
            })
        }

        var index = 100
        new_add.setText("New").setonClickListener {// val randomInt = random.nextint (0, 0, 0, 0, 0).setonClickListener {// val randomInt = Random.if (mSortedListAdapter.size  0) mSortedListAdapter.size else 10)
            mSortedListAdapter.add(SortedItemViewModelTest().apply {
                model = SortedModelTest(randomInt, "Title$randomInt"."SortId is the same as uniqueId, trigger replacement."}) // To update the data based on the uniqueId, Msortedlistadapter.add (SortedItemViewModelTest().apply {model = SortedModelTest(index++,"Title$randomInt"."SortId is different than uniqueId.")
            })
        }

        delete.setText("Delete").setOnClickListener {
            if (mSortedListAdapter.size  0) {
                val randomInt = Random.nextInt(0, mSortedListAdapter.size)
                mSortedListAdapter.removeItemAt(randomInt)
            }
        }

        update.setText("Replacement").setonClickListener {// replace by uniqueId. If sortId is different, the order will be triggeredif(mSortedListAdapter.size0){
                val randomInt = Random.nextInt(0, mSortedListAdapter.size)
                mSortedListAdapter.updateItem(randomInt, SortedItemViewModelTest().apply {
                    model = SortedModelTest(randomInt, "Title$randomInt"."Replace the title with the uniqueId${index++}")})}}}}Copy the code

conclusion

Or that sentence, not the best choice, only the most suitable, also hope I can do for you, for the reconstruction of the ViewHolder reference in the implementation of https://github.com/mikepenz/FastAdapter, I couldn't find a more appropriate choice. Of course, there will be unreasonable design, if you feel bad, please tell me, thank you ?

Last blog

A senior Android should learn to do a super RecyclerView.Adapter

developers

Search
About
mo4tech.com (Moment For Technology) is a global community with thousands techies from across the global hang out!Passionate technologists, be it gadget freaks, tech enthusiasts, coders, technopreneurs, or CIOs, you would find them all here.