RecyclerView+ListAdapter加载条目时页面切换卡顿问题排查
底部导航切换RecyclerView页面时的卡顿问题
我的应用有带底部导航栏的仪表盘,共5个页面。其中一个页面的RecyclerView要加载约20个条目,点击底部导航切换到该页面时会出现微卡顿,破坏了页面切换的流畅性(底部导航带有动画效果),如果加载100个条目,卡顿会更严重。
我是不是操作有误?该怎么消除这种微卡顿?我猜测应用需要时间渲染列表,但不想让全局UI冻结,希望只让RecyclerView延迟渲染,不阻塞UI。
现有代码
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupView() setupObservers() } private fun setupView() { adapterRecipeSections = RecipeCategoriesAdapter(::onItemClick) binding.recyclerRecipeSections.apply { layoutManager = GridLayoutManager(requireContext(), 2, GridLayoutManager.VERTICAL, false) addItemDecoration( GridSpacingItemDecoration(2, 8, requireContext()) ) adapter = adapterRecipeSections } } private fun setupObservers() { viewModel.recipesContent.observe(viewLifecycleOwner) { adapterRecipeSections.submitList(it) } }
Adapter
class RecipeCategoriesAdapter( private val onItemClick: () -> Unit ) : ListAdapter<RecipeItemUI, RecipeCategoriesAdapter.ViewHolder>(StandardDiffUtil()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val itemBinding = CardTitleAndSubtitleBottomWithBackgroundBinding .inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(itemBinding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bindItem(getItem(position), onItemClick) } class ViewHolder( private val itemBinding: CardTitleAndSubtitleBottomWithBackgroundBinding ) : RecyclerView.ViewHolder(itemBinding.root) { fun bindItem(section: RecipeItemUI, onItemClick: () -> Unit) { ... } } }
ViewModel
private val _recipesContent: MutableLiveData<MutableList<RecipeItemUI>> = MutableLiveData() val recipesContent: LiveData<MutableList<RecipeItemUI>> = _recipesContent init { fetchRecipesContent() } private fun fetchRecipesContent() { viewModelScope.launch { _recipesContent.value = dashboardRepo.fetchRecipeContent() } }
Repository
override suspend fun fetchRecipeContent(): MutableList<RecipeItemUI> { return withContext(Dispatchers.IO) { mutableListOf( RecipeItemUI("Colazione", ""), RecipeItemUI("Dolci", ""), RecipeItemUI("Aperitivi", ""), RecipeItemUI("Long drinks", ""), RecipeItemUI("Sour", ""), RecipeItemUI("Sparkling", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Colazione", ""), RecipeItemUI("Dolci", "") ) } }
问题分析与解决办法
1. 调整数据加载时机
当前ViewModel在初始化阶段就触发数据加载,Fragment创建后立刻收到数据并提交给RecyclerView,刚好和底部导航的切换动画抢占UI线程资源,导致卡顿。可以改成页面完全可见后再加载/提交数据:
优化后的Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupView() } override fun onStart() { super.onStart() // 页面可见时才触发数据加载 viewModel.fetchRecipesContent() setupObservers() } private fun setupObservers() { viewModel.recipesContent.observe(viewLifecycleOwner) { adapterRecipeSections.submitList(it) } }
优化后的ViewModel
private val _recipesContent: MutableLiveData<List<RecipeItemUI>> = MutableLiveData() val recipesContent: LiveData<List<RecipeItemUI>> = _recipesContent // 移除init中的自动加载,由Fragment在页面可见时调用 fun fetchRecipesContent() { viewModelScope.launch(Dispatchers.IO) { val result = dashboardRepo.fetchRecipeContent() withContext(Dispatchers.Main) { _recipesContent.value = result } } }
2. RecyclerView渲染优化
- 开启预取与缓存:让RecyclerView提前缓存ViewHolder,减少重复创建的开销
private fun setupView() { adapterRecipeSections = RecipeCategoriesAdapter(::onItemClick) binding.recyclerRecipeSections.apply { layoutManager = GridLayoutManager(requireContext(), 2, GridLayoutManager.VERTICAL, false) addItemDecoration(GridSpacingItemDecoration(2, 8, requireContext())) adapter = adapterRecipeSections // 开启条目预取 isItemPrefetchEnabled = true // 设置缓存ViewHolder数量,根据网格列数调整 setItemViewCacheSize(4) } }
- 简化Item布局:减少布局嵌套层数,用
ConstraintLayout替代多层LinearLayout/RelativeLayout,降低Measure和Layout阶段的耗时
3. 延迟列表提交时机
如果页面切换动画还是卡顿,可以延迟RecyclerView的列表提交,等动画完成后再渲染:
private fun setupObservers() { viewModel.recipesContent.observe(viewLifecycleOwner) { // 延迟150ms提交,避开动画的UI线程高峰 binding.recyclerRecipeSections.postDelayed({ adapterRecipeSections.submitList(it) }, 150) } }
4. 优化DiffUtil计算
如果自定义的StandardDiffUtil逻辑复杂,可以手动在后台线程完成Diff计算,避免占用UI线程:
// ViewModel中 fun fetchRecipesContent() { viewModelScope.launch(Dispatchers.IO) { val newList = dashboardRepo.fetchRecipeContent() val oldList = _recipesContent.value ?: emptyList() val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getOldListSize() = oldList.size override fun getNewListSize() = newList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].id == newList[newItemPosition].id } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition] == newList[newItemPosition] } }) withContext(Dispatchers.Main) { diffResult.dispatchUpdatesTo(adapterRecipeSections) _recipesContent.value = newList } } }
内容的提问来源于stack exchange,提问作者giordy16
相关产品推荐
相关产品推荐

