Home > Mobile >  onCreateViewHolder() of child recycler view not being called in Android [Kotlin]
onCreateViewHolder() of child recycler view not being called in Android [Kotlin]

Time:08-11

I have a nested recycler view structure. The data for the recycler view is coming from Room Database and it follows MVVM architecture with repositories and viewmodels.

I am able to inflate the parent recycler view and get the list on screen but the child recycler view is not being inflated. I can see the value being passed through the adapter by using logs. But that is not being used on onBindViewHolder() and neither onCreateViewHolder() is called

Any help would be appreciated.

My CODE:

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

    //rooms adapter
    private lateinit var roomAdapter: RoomsAdapter

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

        roomAdapter = RoomsAdapter()
        return binding.root
    }

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

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

        ListItemControlPanelFloorsBinding.inflate(layoutInflater).apply {
            rvRoomControlPanel.adapter = roomAdapter
            rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
            Timber.d("Inside list item")
        }

        controlPanelViewModel.getAllFloors.observe(viewLifecycleOwner, Observer {
            Timber.d("List is $it")
            //Remove duplicates from received list
            val distinct = it.toSet().toList().sorted()

            Timber.d("List after removing duplicates and sorting: $distinct")
            adapter.floorList(distinct)

            for (i in distinct) {
                controlPanelViewModel.getAllRooms(i).observe(viewLifecycleOwner, Observer { rooms ->
                    Timber.d("Floor: $i, Rooms: $rooms")

                    val distinctRooms = rooms.toSet().toList()
                    roomAdapter.roomList(distinctRooms)
                })
            }
        })
    }

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

Parent recycler view adapter

open class FloorsAdapter() : RecyclerView.Adapter<FloorsAdapter.FloorViewHolder>() {

    private var floorList = emptyList<String>()

    inner class FloorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {}

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

    @SuppressLint("NotifyDataSetChanged")
    override fun onBindViewHolder(holder: FloorsAdapter.FloorViewHolder, position: Int) {
        val item = floorList[position]
        Timber.d("Current floor is $item, Floor List is : $floorList")
        ListItemControlPanelFloorsBinding.bind(holder.itemView).apply {
            Timber.d("Current floor is $item, Floor List is : $floorList")

            tvFloor.text = "Floor : $item"
        }
    }

    @SuppressLint("NotifyDataSetChanged")
    fun floorList(floors: List<String>) {
        this.floorList = floors
        notifyDataSetChanged()
    }

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

Child Recycler view adapter:

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

    private var roomList = emptyList<String>()

    inner class RoomsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

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

    override fun onBindViewHolder(holder: RoomsViewHolder, position: Int) {
        Timber.d("Room List onBindViewHolder: $roomList")
        val item = roomList[position]
        ListItemControlPanelRoomsBinding.bind(holder.itemView).apply {
            tvRoom.text = item
        }
    }

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

    @SuppressLint("NotifyDataSetChanged")
    fun roomList(room: List<String>) {
        this.roomList = room
        Timber.d("Room List: $roomList")
        notifyDataSetChanged()
    }


}

Fragment layout.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: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>

List Item for fragment recycler view(parent recycler view)

<?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>

List item for child recycler view

<?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> 

Edit 1: My 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 :

    //returns all rooms associated with a floor
    @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 instance
    val repository: ControlPanelRepository

    //Variable for getting all floors
    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)
    }


}

CodePudding user response:

The problem is that each "floor" (row in the floor RecyclerView) has its own unique "room" RecyclerView, but you never attach an adapter to those room RecyclerViews. Instead, you inflate an unused "floor" layout in the Fragment and attach a room adapter to it. That layout will never be displayed anywhere so its adapter is never used.

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

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

    // The problem is this part.
    // This view isn't shown anywhere, and the views inflated in FloorsAdapter
    // never get a RoomAdapter attached
    ListItemControlPanelFloorsBinding.inflate(layoutInflater).apply {
        rvRoomControlPanel.adapter = roomAdapter
        rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
        Timber.d("Inside list item")
    }

Each displayed floor layout is inflated in the floor adapter in onCreateViewHolder and the data for the floor then gets set in onBindViewHolder. This means you need to create a separate RoomsAdapter for each floor and attach it inside the floor adapter somewhere.

This makes accessing the room adapters more difficult with your current pattern, but if you make a Floor object that contains both its name and its list of rooms, then the floor object selected in onBindViewHolder can pass the list of rooms to its RoomAdapter. This means the FloorsAdapter would take a List<Floor> instead of List<String>. Then in the main fragment you only need to pass the list of Floor objects to the floor adapter and it will handle passing room lists on to the room adapters appropriately.

data class Floor(val name: String, val rooms: List<String>) {}

The floor adapter would look something like this:

// modify the ViewHolder to store the binding (so you don't have to
// re-bind the views so often) and store the room adapter for this
// floor. There should be one room adapter per floor.
inner class FloorViewHolder(val binding: ListItemControlPanelFloorsBinding, 
                            val roomAdapter: RoomsAdapter)
                            : RecyclerView.ViewHolder(binding.root) {}

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

    // Create one adapter per floor here
    val adapter = RoomsAdapter()
    binding.rvRoomControlPanel.adapter = adapter
    binding.rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
    return FloorViewHolder(binding, adapter)
}

override fun onBindViewHolder(holder: FloorsAdapter.FloorViewHolder, position: Int) {
    // for a given floor, set the rooms on that floor's room adapter
    val currentFloor = floorList[position]
    holder.roomAdapter.roomList(currentFloor.rooms)
    holder.binding.tvFloor.text = "Floor : ${currentFloor.name}"
}

Here's an example of how that might look in the ViewModel, when it constructs the list of Floor objects, and the activity would observe the floorList LiveData. Each Floor contains all the data (including nested data) that it needs to display.

private val floorListLiveData = MutableLiveData<List<Floor>>()
val floorList: LiveData<List<Floor>>
    get() = floorListLiveData
        
fun load() {
    viewModelScope.launch {
        val floors = mutableListOf<Floor>()
        
        val floorList = dao.readFloors()
        
        for(floorName in floorList) {
            val rooms = dao.readRoomsInFloor(floorName)
            floors.add(Floor(floorName, rooms))
        }
        
        floorListLiveData.postValue(floors)
    }
}
  • Related