Efficient reuse: Optimization tips when horizontal lists are nested inside RecyclerView | Developers say·DTalk

c4894eee07a26a41fbf6e639d7d31e17.jpeg

Original author of this article: Little Horse Run, Original textPublished on: Code Talk

183bbd9134549343018603e333e004da.png

Background

Suppose you want to achieve the following renderings:

5095789da17e4da7ddd8778d82bbe680.png

As shown in the figure, first of all, this is a multi-style sliding list (only 3 styles are listed in the screenshot). There is no doubt that the overall external use of RecyclerView is used to implement it. So how to implement the horizontal label list pointed by the arrow in the third ItemView in the screenshot?

eeba3d6dd6f19d9a80f6912ee7eaed8e.png

Implementation ideas

We make an abstraction of the above problem, which is essentially two lists: The outer one is a vertical list, and the inner one has a horizontal list. as follows:

c999b3cb80f569caccfecd90048b643c.png

The key code of the external vertical list is implemented as follows:

//RecyclerView.Adapter
open class BaseAdapter<T : Any>(private val vhFactory: IVHFactory) :
    RecyclerView.Adapter<BaseVHolder<T>>() {


    private val models = mutableListOf<T>()


    override fun getItemViewType(position: Int): Int {
        val model = models[position]
        if (model is IMultiType) return model.getItemViewType()
        return super.getItemViewType(position)
    }


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVHolder<T> {
        //Create ViewHolder here
        return vhFactory.getVH(parent.context, parent, viewType) as BaseVHolder<T>
    }


    override fun onBindViewHolder(holder: BaseVHolder<T>, position: Int) {
        //Bind data here
        holder.onBindViewHolder(models[position], position)
    }


    override fun getItemCount(): Int = models.size


    fun submitList(newList: List<T>) {
        //Pass in new and old data for comparison
        val diffUtil = ChatDiffUtil(models, newList)
        //Get the difference results after comparison
        val diffResult = DiffUtil.calculateDiff(diffUtil)
        //NOTE: Note that the data in the Adapter needs to be reset here
        models.clear()
        models.addAll(newList)
        //Transfer the data to the adapter, and finally update the data through adapter.notifyItemXXX
        diffResult.dispatchUpdatesTo(this)
    }
}


//Factory mode, used to produce BaseVHolder
interface IVHFactory {
    fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*>
}
  • onCreateViewHolder() is used to create ViewHolder objects. It is called every time a new ItemView is needed and returns a ViewHolder object containing the ItemView.

  • onBindViewHolder() is responsible for associating data with the ItemView view at the specified position. This function will be called multiple times to update the display content when scrolling the list.

class ChatVHolderFactory : IVHFactory {
    companion object {
        const val TYPE_ASK_TXT = 1 //type1
        const val TYPE_REPLY_TXT = 2 //type2
        const val TYPE_REPLY_SPAN = 3 //type3
    }


    override fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*> {
        return when (viewType) {
            TYPE_ASK_TXT -> ChatAskHolder(context, parent)
            TYPE_REPLY_TXT -> ChatReplyTxHolder(context, parent)
            TYPE_REPLY_SPAN -> ChatReplyImgTextHolder(context, parent)
            else -> throw IllegalStateException("unSupport type")
        }
    }
}


class ChatGptActivity : AppCompatActivity() {


    private val mRv: RecyclerView by id(R.id.rv_view)
    private val chatAdapter by lazy { BaseAdapter<MessageModel>(ChatVHolderFactory()) }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_layout_rv)
        setRvInfo()
    }


    private fun setRvInfo() {
        val list = mutableListOf<MessageModel>()
        list.add(MessageModel(content = "Weather Forecast", type = ChatVHolderFactory.TYPE_ASK_TXT))
        list.add(MessageModel(content = "The weather conditions are as follows:", type = ChatVHolderFactory.TYPE_REPLY_TXT))
        list.add(MessageModel(type = ChatVHolderFactory.TYPE_REPLY_SPAN))
        for (i in 0..20) {
            list.add(MessageModel(content = "Weather Forecast", type = ChatVHolderFactory.TYPE_ASK_TXT))
        }
        chatAdapter.submitList(list)
        mRv.layoutManager = LinearLayoutManager(this)
        mRv.adapter = chatAdapter
    }

The above code is a simple encapsulation of the multi-type list scenario, and no further explanation will be given.

Focus on how to implement the horizontal list inside the third ItemView. There are two situations for the number of horizontal label lists:

  • case 1: The number of tag lists is fixed;

  • Case 2: The number of tag lists is not fixed (the data is delivered by the server). If it is not fixed, then the list should be dynamically created after getting the data in Adapter#onBindViewHolder.

The following possible implementation methods are obtained for different situations.

Method 1

The label list is directly implemented using a fixed number of TextView controls, which can meet the scenario of case 1. You don’t have to think about anything, just do it!

It is also very convenient to use, because it does not involve dynamic creation, so there will be no problem of frequently creating sub-Views when sliding up and down. However, this implementation method has shortcomings:

  • Multiple TextView objects need to be created and each object reference needs to be assigned a value one by one.

  • It is not flexible enough and is useless when the number of tag lists is not fixed.

Method 2

Use a LinearLayout parent ViewGroup to dynamically add sub-views for each label. The key code is as follows:

private val labels = mutableListOf<CardItemModel>().apply {
        add(CardItemModel().apply { sceneName = "Tag 1" })
        add(CardItemModel().apply { sceneName = "Tag 2" })
        add(CardItemModel().apply { sceneName = "Tag 3" })
        add(CardItemModel().apply { sceneName = "Tag 4" })
    }
  private val llLabel: LinearLayoutCompat = bind(R.id.ll_label)


  llLabel.removeAllViews()
  llLabel.weightSum = 1F
  labels.forEachIndexed { index, it ->
     val itemView = LayoutInflater.from(context).inflate(R.layout.chat_reply_language_change_item, null)
     val tv: TextView = itemView.findViewById(R.id.tv_language)
     tv.text = it.sceneName
     //Add label subView
     log("Method 2: LinearLayout.addView $index")
     llLabel.addView(itemView, LinearLayoutCompat.LayoutParams(
     0, ViewGroup.LayoutParams.WRAP_CONTENT, 1 / labels.size.toFloat()).apply {
     if (index != labels.lastIndex) marginEnd = 10.dp2px() })
  }

Method 3

The internal horizontal tab list is also implemented using RecyclerView. Pay attention to the usage details. We need to use DiffUtil to update the data. The advantage of this is that we can take advantage of the reuse mechanism of RecyclerView and DiffUtil Improving performance. The key code is as follows:

class ChatDiffUtil(private val oldModels: List<Any>, private val newModels: List<Any>):
    DiffUtil.Callback() {


    /**
     *Old data
     */
    override fun getOldListSize(): Int = oldModels.size


    /**
     *New data
     */
    override fun getNewListSize(): Int = newModels.size


    /**
     * DiffUtil is called to determine whether two objects represent the same Item. true means the two items are the same (meaning the View can be reused), false means they are different (the View cannot be reused)
     * For example, if your items have unique ids, this method should check if their ids are equal.
     */
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldModels[oldItemPosition]::class.java == newModels[newItemPosition]::class.java
    }


    /**
     * Compare whether two Items have the same content (used to determine whether the content of the Item has changed),
     * This method will only be called when areItemsTheSame (int, int) returns true.
     */
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldModels[oldItemPosition] == newModels[newItemPosition]
    }


    /**
     * When this method is executed: areItemsTheSame(int, int) returns true and areContentsTheSame(int, int) returns false
     * This method returns the changed data in the Item, which is used to update only the UI corresponding to the changed data in the Item.
     */
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        return super.getChangePayload(oldItemPosition, newItemPosition)
    }
}
//Declare BaseViewHolder for easy use later
//BaseViewHolder
abstract class BaseVHolder<T>(context: Context, parent: ViewGroup, resource: Int):
    RecyclerView.ViewHolder(LayoutInflater.from(context).inflate(resource, parent, false)) {


    fun onBindViewHolder(item: T, position: Int) {
        onBindView(item, position)
    }


    abstract fun onBindView(item: T, position: Int)


    protected fun <V : View> bind(id: Int): V {
        return itemView.findViewById(id)
    }
}

use it:

//ViewHolder
class LabelItemHolder(
        context: Context,
        parent: ViewGroup,
        layoutId: Int = R.layout.chat_reply_language_change_item,
    ) : BaseVHolder<CardItemModel>(context, parent, layoutId) {


        private val sceneName = bind<TextView>(R.id.tv_language)


        override fun onBindView(item: CardItemModel, position: Int) {
            log("Method 3: onBindViewHolder: $position")
            sceneName.text = item.sceneName
        }
    }


//Declare Adapter
private val labelAdapter by lazy {
        BaseAdapter<CardItemModel>(object : IVHFactory{
            override fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*> {
                log("Method 3: onCreateViewHolder")
                return LabelItemHolder(context, parent)
            }
        })
    }


private val labels = mutableListOf<CardItemModel>().apply {
        add(CardItemModel().apply { sceneName = "Tag 1" })
        add(CardItemModel().apply { sceneName = "Tag 2" })
        add(CardItemModel().apply { sceneName = "Tag 3" })
        add(CardItemModel().apply { sceneName = "Tag 4" })
    }


//Refresh the list data in onBindViewHolder() in the external Adapter
labelAdapter.submitList(labels)

f618fb7fe94192aa4e90c88c336f28a2.png

Performance comparison

5d84078dc50b87d488de90e74bf5bec9.png

Effect comparison

The above screenshots are the UI effects achieved using methods 2 and 3. Method 1 is not flexible enough, so I won’t look at it anymore. Let’s compare the performance of Method 2 and Method 3. When the page is opened for the first time, the log output is as follows:

E/Tag: External Rv---> onBindViewHolder(): 2


E/Tag: Method 2: LinearLayout.addView 0
E/Tag: Method 2: LinearLayout.addView 1
E/Tag: Method 2: LinearLayout.addView 2
E/Tag: Method 2: LinearLayout.addView 3


E/Tag: Method 3: onCreateViewHolder
E/Tag: Method 3: onBindViewHolder: 0
E/Tag: Method 3: onCreateViewHolder
E/Tag: Method 3: onBindViewHolder: 1
E/Tag: Method 3: onCreateViewHolder
E/Tag: Method 3: onBindViewHolder: 2
E/Tag: Method 3: onCreateViewHolder
E/Tag: Method 3: onBindViewHolder: 3

Because it is the first time to create, in method 2, each label sub-View is added through LinearLayout#addView, while in method 3, it is created through onCreateViewHolder and onBindViewHolder in RecyclerView.Adapter. Assuming that the list is long enough, continue to slide down and then slide back. The log at this time is as follows:

E/Tag: External Rv---> onBindViewHolder(): 2
E/Tag: Method 2: LinearLayout.addView 0
E/Tag: Method 2: LinearLayout.addView 1
E/Tag: Method 2: LinearLayout.addView 2
E/Tag: Method 2: LinearLayout.addView 3

You can see that when the list slides to the original position again, the label sub-View will be re-created each time in method 2, but it will not be re-created in method 3. This is because when the data is set again through DiffUtil in method 3, the data will be compared. , if the data has not changed, nothing will be done. When we first created the View, we had already set data for each sub-View, so the data displayed at this time is still correct.

I have a question here. Why is it that when I slide the list up and down and return to the original position, method 3 can display it correctly without resetting the data? We know that RecyclerView is a ViewHolder cached through RecyclerView.Recycler. When trying to get the itemView in ViewHolder, the following method will be called:

//RecyclerView.Recycler
@NonNull
 public View getViewForPosition(int position) {
      return getViewForPosition(position, false);
 }


 View getViewForPosition(int position, boolean dryRun) {
     return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

When sliding up and down, our ViewHolder will be cached step by step. Assume that it is finally stored in mRecyclerPool. At this time, because the ItemView set the data when it was first created, the bound data will be stored in the ViewHolder together. Therefore, when you slide to the original position again, although the data is not set, the data will be obtained from the cache pool and displayed correctly.

Hereyou can regardViewHolder as an ordinary object. When caching, not only cacheItemView, If the data has been set before, it will also be cached.

93a925e7e2c55271dc02be413b0d2179.png

△ RecyclerView caching mechanism

a46f4235fa30c9d90e9a652656725837.png

Summary

For a nested horizontal list of an ItemView inside RecyclerView, the following methods are usually considered:

  • Directly create multiple fixedsub-views: This method is not flexible enough and has poor scalability, and it is powerless when dynamically creating sub-views;

  • Dynamicly create eachsub-View throughViewGroup method: This method itself cannot cache the sub-View, so each time The sub-View will be re-created when sliding up and down. Although it can achieve the effect we want, the performance is not optimal;

  • Create an internal list through RecyclerView and use DiffUtil for data comparison and update operations: Update when the data changes, otherwise do nothing. This can make the most of RecyclerView’s reuse mechanism and caching advantages, accurately refresh when data changes, and improve overall rendering efficiency. So this method is the optimal solution.

0370b00c0f0e915c730acdb4c8e067d8.png

Example address

For complete code examples see:

https://github.com/crazyqiang/AndroidStudy

Long press the QR code on the right

View more exciting sharing from developers

8bc25a5526758249b883ea51c0acc0ed.png

“Developers say·DTalk” For 5427a5fae31de0f9d404690012dd9a02.png\ China Developers solicit product/technical content related to Google mobile applications (apps & games). Everyone is welcome to come and share your industry insights or opinions on mobile applications, experiences or new discoveries in the mobile development process, as well as practical experience summaries of overseas applications and feedback on the use of related products. We sincerely hope to provide these outstanding Chinese developers with a platform to better showcase themselves and give full play to their strengths. We will focus on selecting excellent cases through your technical content for recommendation by Google Development Technology Experts (GDE).

b5f810b8d2be615612a6705ec70b3958.gif Click at the end of the screen < strong> | Read the original text | Sign up nowDevelopers say·DTalk”

a43bcb0b69e38761b062e3a4e78858b3.png