RecyclerView首屏元素未加载,滚动后才显示的问题求助
RecyclerView首屏元素启动不加载,滚动后才显示的问题
问题描述
我在个人项目中遇到一个Bug: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
相关产品推荐
相关产品推荐

