You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView首屏元素未加载,滚动后才显示的问题求助

RecyclerView首屏元素启动不加载,滚动后才显示的问题

问题描述

我在个人项目中遇到一个Bug:RecyclerView的首屏元素无法在启动时加载,只有向下滚动再滚回后才会显示。其余元素加载正常,不确定问题原因,是否需要限制RecyclerView的元素数量?

问题演示

RecyclerView启动不加载首屏元素演示

相关代码

SchoolListAdapter.kt

package com.example.nycschools.view

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.navigation.findNavController
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.nycschools.R
import com.example.nycschools.databinding.SchoolItemBinding
import com.example.nycschools.model.NYCSchools

/**
 * this class implements a [RecyclerView] [ListAdapter] which uses DataBinding to present [List] of [NYCSchools] data
 */

class SchoolListAdapter : ListAdapter<NYCSchools, SchoolListAdapter.SchoolListViewHolder>(DiffCallBack) {

    /**
     * Creates new [RecyclerView] item views
     */

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SchoolListAdapter.SchoolListViewHolder {
        return SchoolListViewHolder(SchoolItemBinding.inflate(LayoutInflater.from(parent.context)))
    }

    /**
     * Allows the [RecyclerView] to determine which items have changed when the [List] of [NYCSchools] has updated
     */

    companion object DiffCallBack: DiffUtil.ItemCallback<NYCSchools>() {
        override fun areItemsTheSame(oldItem: NYCSchools, newItem: NYCSchools): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: NYCSchools, newItem: NYCSchools): Boolean {
            return oldItem.schoolName == newItem.schoolName
        }
    }

    /**
     * Replaces the contents of a view
     */
    override fun onBindViewHolder(holder: SchoolListAdapter.SchoolListViewHolder, position: Int) {
        val nycSchools = getItem(position)
        holder.bind(nycSchools)
    }

    /**
     * This takes the binding variable from the associated [SchoolItemBinding], which gives it access to the full [NYCSchools] information
     * This also sends the databaseName to the schoolInfoFragment through Jetpack Navigation actions + arguments so the look up for the SAT Scores can be done
     */

    class SchoolListViewHolder(private val binding: SchoolItemBinding):
        RecyclerView.ViewHolder(binding.root) {
            fun bind(nycSchools: NYCSchools) {
                binding.buttonItem.text = nycSchools.schoolName
                binding.buttonItem.setOnClickListener {
                    binding.buttonItem.findNavController().navigate(R.id.action_schoolListFragment_to_schoolInfoFragment,
                        Bundle().apply {
                            putString("databaseName",nycSchools.id)
                        }
                    )
                }
                binding.executePendingBindings()
            }
    }
}

SchoolListFragment.kt

package com.example.nycschools.view

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import com.example.nycschools.R
import com.example.nycschools.databinding.FragmentSchoolListBinding
import com.example.nycschools.viewmodels.NYCSchoolViewModel

class SchoolListFragment: Fragment() {

    private val viewModel: NYCSchoolViewModel by activityViewModels()
    private var binding: FragmentSchoolListBinding? = null

    /**
     * Creates the View, and inflates it on to the MainActivity
     */

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val fragmentBinding = FragmentSchoolListBinding.inflate(inflater,container,false)
        binding = fragmentBinding
        return fragmentBinding.root
    }

    /**
     * Called after the onCreateView to make sure the Fragments root is non-null
     */

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding?.viewModel = viewModel
        binding?.schoolListFragment = this
        binding?.lifecycleOwner = this
        binding?.schoolList?.adapter = SchoolListAdapter()
    }

    /**
     * This fragment lifecycle method is called when the view hierarchy associated with the fragment
     * is being removed. As a result, clear out the binding object.
     */

    override fun onDestroyView() {
        super.onDestroyView()
        binding = null
    }
}

BindingAdapter.kt

package com.example.nycschools.view

import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.nycschools.model.NYCSchools


/**
 * Updates the data shown in the [RecyclerView]
 */

@BindingAdapter("schoolListData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<NYCSchools>?) {
    val adapter = recyclerView.adapter as SchoolListAdapter
    adapter.submitList(data)
}

NYCSchools.kt

data class NYCSchools (
    @Json(name = "dbn") val id: String,
    @Json(name = "school_name") val schoolName: String)

NYCSchoolsAPIService.kt

package com.example.nycschools.API

import com.example.nycschools.model.NYCSchools
import com.example.nycschools.model.NYCSchoolsWithSAT
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET

private const val BASE_URL = "https://data.cityofnewyork.us/resource/"

/**
 * Building a Moshi object with a Kotlin adapter factory that retrofit will be using
 */

private val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

/**
 * Building the retrofit object with the moshi converter
 */

private val retrofit = Retrofit.Builder()
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .baseUrl(BASE_URL)
    .build()

/**
 * A Public interface that is used to get the Json Data for our Models
 *  NYCSchools
 *  NYCSchoolsWithSAT
 */

interface NYCSchoolsApiService {
    @GET("s3k6-pzi2")
    suspend fun getNYCSchools(): List<NYCSchools>

    @GET("f9bf-2cp4")
    suspend fun getNYCSchoolsWithSAT(): List<NYCSchoolsWithSAT>
}

/**
 * A public Api object that exposes the lazy-initialized Retrofit service
 * Basically a Singleton object (only gets called once)
 */

object NYCSchoolsAPI {
    val retrofitService: NYCSchoolsApiService by lazy { retrofit.create(NYCSchoolsApiService::class.java) }
}

Fragment_school_list.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewModel"
            type="com.example.nycschools.viewmodels.NYCSchoolViewModel" />
        <variable
            name="schoolListFragment"
            type="com.example.nycschools.view.SchoolListFragment" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".view.SchoolListFragment">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/schoolList"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:schoolListData="@{viewModel.nycSchools}"
            android:clipToPadding="false"
            android:padding="16dp"
            tools:itemCount="12"
            tools:listitem="@layout/school_item"/>

    </FrameLayout>
</layout>

问题分析与解决方案

不需要限制RecyclerView的元素数量,问题核心出在数据提交逻辑和布局测量异常上,以下是具体修复步骤:

1. 修正ViewModel中LiveData的初始化与更新逻辑

确保nycSchools是MutableLiveData类型,并且在数据加载完成后正确触发更新。如果初始值为null,可能导致BindingAdapter提交空数据,后续更新未触发RecyclerView刷新。示例代码:

// 在NYCSchoolViewModel中
val nycSchools = MutableLiveData<List<NYCSchools>>()

fun loadSchools() {
    viewModelScope.launch {
        try {
            val schools = NYCSchoolsAPI.retrofitService.getNYCSchools()
            nycSchools.postValue(schools) // 用postValue确保在主线程更新
        } catch (e: Exception) {
            // 处理网络请求异常
        }
    }
}

2. 优化BindingAdapter的空数据处理

当前BindingAdapter在data为null时直接提交,会导致ListAdapter接收到null,影响初始渲染。修改为提交空列表而非null:

@BindingAdapter("schoolListData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<NYCSchools>?) {
    val adapter = recyclerView.adapter as SchoolListAdapter
    adapter.submitList(data ?: emptyList()) // 空数据时提交空列表
}

3. 修复RecyclerView的布局约束问题

XML中RecyclerView用FrameLayout包裹,但同时设置了ConstraintLayout的约束属性,FrameLayout不支持这些约束,会导致RecyclerView初始测量异常。两种修复方式:

  • 方式一:将外层FrameLayout替换为ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".view.SchoolListFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/schoolList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:schoolListData="@{viewModel.nycSchools}"
        android:clipToPadding="false"
        android:padding="16dp"
        tools:itemCount="12"
        tools:listitem="@layout/school_item"/>

</androidx.constraintlayout.widget.ConstraintLayout>
  • 方式二:保留FrameLayout,去掉多余的约束属性
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".view.SchoolListFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/schoolList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
        app:schoolListData="@{viewModel.nycSchools}"
        android:clipToPadding="false"
        android:padding="16dp"
        tools:itemCount="12"
        tools:listitem="@layout/school_item"/>

</FrameLayout>

4. 补充手动数据观察(可选)

在Fragment的onViewCreated中添加LiveData观察,作为BindingAdapter的补充,确保数据变化能被Adapter捕获:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding?.viewModel = viewModel
    binding?.schoolListFragment = this
    binding?.lifecycleOwner = this
    binding?.schoolList?.adapter = SchoolListAdapter()
    
    // 手动观察数据变化
    viewModel.nycSchools.observe(viewLifecycleOwner) { schools ->
        (binding?.schoolList?.adapter as? SchoolListAdapter)?.submitList(schools ?: emptyList())
    }
}

内容的提问来源于stack exchange,提问作者Karan Vadhan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 06:41:34