简体   繁体   中英

Recyclerview - onCreateViewHolder called for each list item

I have implemented a simple adapter but it is causing RecyclerView not to recycler views and calls onCreateViewHolder() for every list item when scrolled. This causes jank whenever I scroll the list. Few points listed below are not related to excessive calls of onCreateViewHolder() , but I tried them to improve scroll performance and avoid jank. Things I have tried so far:

  1. recyclerView.setHasFixedSize(true)
  2. recyclerView.recycledViewPool.setMaxRecycledViews(1, 10) with recyclerView.setItemViewCacheSize(10)
  3. recyclerView.setDrawingCacheEnabled(true) with recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH)
  4. setting RecyclerView height to "match_parent"
  5. Was previously using Kotlin's synthetic, now moved to Android's ViewBinding
  6. Rewrite complex nested layouts to Constraint Layout
  7. override onFailedToRecycleView() to see if it is called, but it was never called

Here is my adapter:

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.suppstore.R
import com.example.suppstore.common.Models.Brand
import com.example.suppstore.databinding.LiBrandBinding
import com.google.firebase.perf.metrics.AddTrace

class BrandsAdapter(list: ArrayList<Brand>, var listener: BrandClickListener?) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private val VIEW_TYPE_LOADING = 0
    private val VIEW_TYPE_NORMAL = 1
    private var brandsList: ArrayList<Brand> = list

    @AddTrace(name = "Brands - onCreateViewHolder", enabled = true)
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (viewType == VIEW_TYPE_NORMAL) {
            ViewHolder(
                LiBrandBinding.inflate(
                    LayoutInflater.from(parent.context),
                    parent, false
                )
            )

        } else {
            LoaderHolder(
                LayoutInflater.from(parent.context)
                    .inflate(R.layout.li_loader, parent, false)
            )
        }
    }

    @AddTrace(name = "Brands - onBindViewHolder", enabled = true)
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is ViewHolder)
            holder.setData(brandsList[position], listener!!)
    }

    class ViewHolder(itemView: LiBrandBinding) : RecyclerView.ViewHolder(itemView.root) {
        private val binding: LiBrandBinding = itemView

        @AddTrace(name = "Brands - ViewHolder-setData", enabled = true)
        fun setData(brand: Brand, listener: BrandClickListener) {
            binding.cardItem.setOnClickListener { listener.onItemClick(brand) }
            binding.tvBrandName.text = brand.name
            binding.tvCount.text = brand.count.toString() + " Products"
        }
    }

    class LoaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView.rootView) {

    }

    @AddTrace(name = "Brands - addLoader", enabled = true)
    fun addLoader() {
        brandsList.add(Brand())
        notifyItemInserted(brandsList.size - 1)

    }

    @AddTrace(name = "Brands - setData", enabled = true)
    fun setData(newList: ArrayList<Brand>) {
        this.brandsList = newList
        notifyDataSetChanged()

    }

    @AddTrace(name = "Brands - removeLoader", enabled = true)
    fun removeLoader() {
        if (brandsList.size == 0)
            return
        val pos = brandsList.size - 1
        brandsList.removeAt(pos)
        notifyItemRemoved(pos)

    }

    override fun getItemViewType(position: Int): Int {
        return if (brandsList.get(position).count == -1) {
            VIEW_TYPE_LOADING
        } else
            VIEW_TYPE_NORMAL

    }

    interface BrandClickListener {
        fun onItemClick(brand: Brand)
    }

    override fun getItemCount(): Int {
        return brandsList.size
    }
}

Here is the list item (li_brand):

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardItem"
    android:layout_width="match_parent"
    android:layout_height="85dp"
    android:background="@color/app_black">

    <TextView
        android:id="@+id/tvBrandName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:textColor="@color/app_yellow"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@id/tvCount"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <TextView
        android:id="@+id/tvCount"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:layout_marginTop="2dp"
        android:textColor="@color/app_grey"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tvBrandName" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="20dp"
        android:layout_gravity="center_vertical"
        android:layout_marginEnd="15dp"
        android:src="@drawable/ic_baseline_arrow_forward_ios_24"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <View
        android:layout_width="match_parent"
        android:layout_height="3dp"
        android:background="@color/app_bg"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Here are related functions in Fragment

class BrandsFragment : Fragment() {
    private val adapter = BrandsAdapter(ArrayList(), brandClickListener())
    fun brandClickListener(): BrandsAdapter.BrandClickListener {
        return object : BrandsAdapter.BrandClickListener {
            override fun onItemClick(brand: Brand) {
                activityViewModel?.setSelectedBrand(brand)
            }
        }
    }

    fun setupRecyclerView() {
        val llManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
        binding.recyclerView.layoutManager = llManager
        binding.recyclerView.setHasFixedSize(true)
        binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                if (dy > 0) { //check for scroll down
                    val visibleItemCount = llManager.childCount
                    val totalItemCount = llManager.itemCount
                    val firstVisibleItemPos = llManager.findFirstVisibleItemPosition()
                    if (loadWhenScrolled
                        && visibleItemCount + firstVisibleItemPos >= totalItemCount
                        && firstVisibleItemPos >= 0
                    ) {
                        //ensures that last item was visible, so fetch next page
                        loadWhenScrolled = false
                        viewModel.nextPage()
                    }
                }
            }
        })
        binding.recyclerView.adapter = adapter
    }
}

And here is the fragment xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/app_black"
    android:focusableInTouchMode="true"
    android:orientation="vertical"
    tools:context=".Brands.BrandsFragment">

    <androidx.appcompat.widget.SearchView
        android:id="@+id/searchView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp"
        android:layout_marginBottom="5dp"
        android:background="@drawable/bottom_line_yellow"
        android:theme="@style/SearchViewTheme"
        app:closeIcon="@drawable/ic_baseline_close_24"
        app:iconifiedByDefault="false"
        app:queryBackground="@android:color/transparent"
        app:queryHint="Atleast 3 characters to search"
        app:searchIcon="@drawable/ic_baseline_search_24" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

Have you tried RecyclerView Diffutil class? Hope it will resolve smooth scrolling issue and overwhelm recreation of items.

https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil

"DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one."

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM