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

Android Kotlin嵌套RecyclerView子项onCreateViewHolder未调用问题

问题排查:嵌套RecyclerView子列表不渲染

问题描述

基于MVVM架构+Room数据库实现嵌套RecyclerView,父RecyclerView可正常加载显示,但子RecyclerView完全不渲染。日志显示数据已传入适配器,但onBindViewHolder和onCreateViewHolder均未触发。


相关代码

Fragment代码

class ControlPanelFragment : Fragment() {

    private var _binding: FragmentControlPanelBinding? = null
    private val binding: FragmentControlPanelBinding get() = _binding!!
    private lateinit var controlPanelViewModel: ControlPanelViewModel

    //floor adapter
    private lateinit var adapter: FloorsAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentControlPanelBinding.inflate(layoutInflater)
        controlPanelViewModel = ViewModelProvider(this)[ControlPanelViewModel::class.java]
        adapter = FloorsAdapter(controlPanelViewModel)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.rvFloors.adapter = adapter
        binding.rvFloors.layoutManager = LinearLayoutManager(requireContext())

        controlPanelViewModel.getAllFloors.observe(viewLifecycleOwner) {
            Timber.d("原始楼层列表: $it")
            //去重并排序
            val distinctFloors = it.toSet().toList().sorted()
            Timber.d("去重排序后的楼层列表: $distinctFloors")
            adapter.updateFloors(distinctFloors)
        }
    }

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

父RecyclerView适配器(修改后)

class FloorsAdapter(private val viewModel: ControlPanelViewModel) : RecyclerView.Adapter<FloorsAdapter.FloorViewHolder>() {

    private var floorList = emptyList<String>()
    //为每个楼层保存对应的房间数据和子适配器
    private val roomAdapters = mutableMapOf<String, RoomsAdapter>()

    inner class FloorViewHolder(private val binding: ListItemControlPanelFloorsBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(floor: String) {
            binding.tvFloor.text = "Floor : $floor"
            
            //初始化子RecyclerView
            if (binding.rvRoomControlPanel.adapter == null) {
                binding.rvRoomControlPanel.layoutManager = LinearLayoutManager(itemView.context)
                val adapter = RoomsAdapter()
                roomAdapters[floor] = adapter
                binding.rvRoomControlPanel.adapter = adapter
            }
            
            //观察当前楼层的房间数据
            viewModel.getAllRooms(floor).observeForever { rooms ->
                Timber.d("楼层: $floor, 房间列表: $rooms")
                val distinctRooms = rooms.toSet().toList()
                roomAdapters[floor]?.updateRooms(distinctRooms)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FloorViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding = ListItemControlPanelFloorsBinding.inflate(layoutInflater, parent, false)
        return FloorViewHolder(binding)
    }

    override fun onBindViewHolder(holder: FloorViewHolder, position: Int) {
        val floor = floorList[position]
        holder.bind(floor)
    }

    fun updateFloors(floors: List<String>) {
        this.floorList = floors
        notifyDataSetChanged()
    }

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

子RecyclerView适配器(修改后)

class RoomsAdapter() : RecyclerView.Adapter<RoomsAdapter.RoomsViewHolder>() {

    private var roomList = emptyList<String>()

    inner class RoomsViewHolder(private val binding: ListItemControlPanelRoomsBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(room: String) {
            binding.tvRoom.text = room
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomsViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        val binding = ListItemControlPanelRoomsBinding.inflate(inflater, parent, false)
        return RoomsViewHolder(binding)
    }

    override fun onBindViewHolder(holder: RoomsViewHolder, position: Int) {
        val room = roomList[position]
        holder.bind(room)
    }

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

    fun updateRooms(rooms: List<String>) {
        this.roomList = rooms
        notifyDataSetChanged()
    }
}

布局文件(保持原结构不变)

Fragment布局

<?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:orientation="vertical"
    tools:context=".ui.controlPanel.ui.ControlPanelFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:elevation="10dp">

        <com.google.android.material.switchmaterial.SwitchMaterial
            android:id="@+id/switchFactory"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="60dp"
            android:checked="false"
            android:text="Factory"
            android:textOff="OFF"
            android:textOn="ON"
            android:textSize="16sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Spinner
            android:id="@+id/spnChooseFloor"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="200dp"
            android:layout_marginEnd="16dp"
            app:layout_constraintBottom_toBottomOf="@+id/switchFactory"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@+id/switchFactory"
            app:layout_constraintTop_toTopOf="@+id/switchFactory" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginStart="16dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="10dp"
        android:elevation="8dp"
        app:cardCornerRadius="8dp">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rvFloors"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:listitem="@layout/list_item_control_panel_floors" />

    </androidx.cardview.widget.CardView>

</LinearLayout>

父RecyclerView列表项布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tvFloor"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="4dp"
        android:text="Floor 1"
        android:textColor="@color/black"
        android:textSize="20sp" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvRoomControlPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        tools:listitem="@layout/list_item_control_panel_rooms" />

</LinearLayout>

子RecyclerView列表项布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="40dp">

    <TextView
        android:id="@+id/tvRoom"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="8dp"
        android:text="Conference Room"
        android:textSize="20sp" />

</LinearLayout>

补充代码(Entity/DAO/Repository/ViewModel)

Entity

data class AddedDevicesInformation(

    @ColumnInfo(name = "floor_name")
    var floorName: String = "",

    @ColumnInfo(name = "room_name")
    var roomName: String = "",

    @ColumnInfo(name = "machine_name")
    var machineName: String = "",

    @ColumnInfo(name = "device_name")
    var deviceName: String = "",

    @ColumnInfo(name = "factory_status")
    var factoryStatus: Boolean? = false,

    @PrimaryKey(autoGenerate = true)
    var id: Int? = null,
)

DAO查询

//返回指定楼层下的所有房间
@Query("SELECT room_name from added_device_information where floor_name =:floor")
fun readAllRoomsOnAFloor(floor: String): LiveData<List<String>>

Repository

class ControlPanelRepository(private val devicesInformationDao: DevicesInformationDao) {

    fun getAllRooms(floor: String): LiveData<List<String>> = devicesInformationDao.readAllRoomsOnAFloor(floor)

}

ViewModel

class ControlPanelViewModel(application: Application) : AndroidViewModel(application) {

    //Repository实例
    val repository: ControlPanelRepository

    //获取所有楼层的LiveData
    val getAllFloors: LiveData<List<String>>

    init {
        val database = PowerManagementDatabase.getDatabase(application)
        val dao = database.getAddedDevicesInformationDao()
        repository = ControlPanelRepository(dao)
        getAllFloors = repository.getAllFloors
    }

    fun getAllRooms(floor: String): LiveData<List<String>> {
        return repository.getAllRooms(floor)
    }

}

问题原因及修复说明

  1. 无效的子RecyclerView初始化:原Fragment中单独Inflate了ListItemControlPanelFloorsBinding并设置适配器,这部分代码完全没用,因为这个Binding并没有关联到父RecyclerView的实际列表项上,直接删除即可。
  2. 共用子适配器导致数据覆盖:原代码中所有父项的子RecyclerView共用一个roomAdapter,最后只会显示最后一组房间数据,改为每个楼层对应独立的子适配器,避免数据冲突。
  3. 子RecyclerView未正确初始化:父适配器的onBindViewHolder中才是初始化子RecyclerView布局管理器和适配器的正确位置,确保每个列表项的子RecyclerView都被正确配置。
  4. LiveData观察优化:将房间数据的观察逻辑移到父适配器的ViewHolder绑定方法中,确保每个楼层只观察自己对应的房间数据,同时避免Fragment中循环观察导致的内存泄漏问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.23 15:57:18