Home > Back-end >  How to handle data in 3 nested recycler views in Android [Kotlin]
How to handle data in 3 nested recycler views in Android [Kotlin]

Time:08-12

I have a structure of three recycler views. So there is one parent recycler view which contains one child recycler view and this child recycler view contains one more child recycler view.

For understanding it's like the first recycler view is the total number of floors. The second recycler view is the total number of rooms and the third recycler view is the total number of devices.

I have a room database from where I can write queries to receive the total floors, rooms, devices as list of strings. As they are stored in database as string values.

For querying floors it's a simple query. For querying rooms I am passing the floor position and for querying the devices attached I am passing the room value.

The project is following MVVM architecture so there are DAO, from where the data goes to repositories which further goes to a Viewmodel and then observed from Fragment using LiveData.

The main Problem : So most of the logic for fetching floor, then room and devices is happening inside the ViewModel. I have tried multiple ways to fetch data from room database by using livedata or suspend functions. But the issue comes while inserting the values in data class in those loops. I am clearly doing something wrong (code given below) because of which the data shown is not in appropriate format. I get the list of floors and rooms but it's like all the rooms are added to each floor instead of them being separated values for each floor. Similarly all the devices which should be attached to the respective rooms are being attached to all the rooms. If someone could help me with this it would be really helpful as I am stuck on this past 2 days. Thanks! All the code is given below and if you need any more understanding let me know in comments.

Entity file

@Entity(tableName = "added_device_information")
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 file

@Dao
interface DevicesInformationDao {

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertAddedDevicesToDatabase(addedDevicesInformation: AddedDevicesInformation)

    //returns all floors in the database
    @Query("SELECT floor_name from added_device_information")
    fun readAllFloorsFromDatabase(): LiveData<List<String>>

    //returns all rooms associated with a floor
    @Query("SELECT room_name from added_device_information where floor_name =:floor")
    suspend fun readAllRoomsOnAFloor(floor: String): List<String>
    
    //Get all devices in a room
    @Query("SELECT device_name from added_device_information where room_name =:room")
    suspend fun readAllDevicesInRoom(room:String): List<String>
}

Repository file

//This repository is for reading values from the database using [DevicesInformationDao]
class ControlPanelRepository(private val devicesInformationDao: DevicesInformationDao) {

    val getAllFloors: LiveData<List<String>> = devicesInformationDao.readAllFloorsFromDatabase()

    //for rooms associated with floors
    suspend fun getAllRooms(floor: String): List<String> = devicesInformationDao.readAllRoomsOnAFloor(floor)

    suspend fun getAllDevices(room: String): List<String> = devicesInformationDao.readAllDevicesInRoom(room)

}

ViewModel file ( The main issue is in this file )

class ControlPanelViewModel(private val repository: ControlPanelRepository) : ViewModel() {

    //Variable for getting all floors
    private val _getAllFloors: LiveData<List<String>> = repository.getAllFloors
    private val getAllFloors: LiveData<List<String>> = _getAllFloors

    //setting FloorDataClass objects
    private val floorListLiveData = MutableLiveData<List<FloorsDataClass>>()
    val floorList: LiveData<List<FloorsDataClass>> get() = floorListLiveData

    fun loadRooms() {

        val floorsList = mutableListOf<FloorsDataClass>()
        val roomsList = mutableListOf<RoomsDataClass>()
        val devicesList = mutableListOf<DevicesDataClass>()

        var distinctFloors: List<String>
        var distinctRooms: List<String>
        var distinctDevices: List<String>

        getAllFloors.observeForever(Observer {

            viewModelScope.launch(Dispatchers.Main) {

                distinctFloors = it.distinct().sorted()

                for (floorName in distinctFloors) {
                    val rooms = repository.getAllRooms(floorName)

                    distinctRooms = rooms.distinct()

                    for (roomName in distinctRooms) {

                        distinctDevices = repository.getAllDevices(roomName)

                        devicesList.add(DevicesDataClass(distinctDevices))

                        roomsList.add(RoomsDataClass(roomName, devicesList))

                        floorsList.add(FloorsDataClass(floorName, roomsList))

                        floorListLiveData.postValue(floorsList)
                        Timber.d("$floorsList")

                    }
                }
            }
        })
    }
}

Fragment File:

 override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentControlPanelBinding.inflate(layoutInflater)

        adapter = FloorsAdapter(activity)
controlPanelViewModel = ViewModelProvider(this, factory)[ControlPanelViewModel::class.java]
        controlPanelViewModel.loadRooms()
        return binding.root
    }


 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.rvFloors.adapter = adapter
        binding.rvFloors.layoutManager = LinearLayoutManager(requireContext())
        binding.rvFloors.setHasFixedSize(true)

        controlPanelViewModel.floorList.observe(viewLifecycleOwner, Observer {
            adapter.floorList(floors = it)
            Timber.d("List is $it")
        })
    }

DATA CLASSES - FloorDataClass

data class FloorsDataClass(val floor: String, val rooms: List<RoomsDataClass>) {
}

RoomDataClass

data class RoomsDataClass(val room:String, val devices: List<DevicesDataClass>) {
}

DeviceDataClass

data class DevicesDataClass(val machine: List<String>) {
}

ADAPTERS FOR RECYCLER VIEWS

  • Floor Adapter
open class FloorsAdapter(
    private val activity: FragmentActivity?
) : RecyclerView.Adapter<FloorsAdapter.FloorViewHolder>() {

    private var floorList = emptyList<FloorsDataClass>()

    inner class FloorViewHolder(
        val binding: ListItemControlPanelFloorsBinding,
        val roomsAdapter: 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 adapter per floor
        val adapter = RoomsAdapter(activity)
        binding.rvRoomControlPanel.adapter = adapter
        binding.rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
        return FloorViewHolder(binding, adapter)
    }

    @SuppressLint("NotifyDataSetChanged", "SetTextI18n")
    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.apply {
            roomsAdapter.roomList(currentFloor.rooms)
            binding.tvFloor.text = "Floor : ${currentFloor.floor}"
            Timber.d("Rooms on floor: $currentFloor are : ${currentFloor.rooms}")
        }
    }

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

    override fun getItemCount(): Int {
        return floorList.size
    }
}
  • RoomAdapter
open class RoomsAdapter(
    private val activity: FragmentActivity?
) : RecyclerView.Adapter<RoomsAdapter.RoomViewHolder>() {

    private var roomList = emptyList<RoomsDataClass>()

    inner class RoomViewHolder(
        val binding: ListItemControlPanelRoomsBinding,
        val machinesAdapter: DevicesAdapter
    ) : RecyclerView.ViewHolder(binding.root) {}

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

        //setting machines for each room
        val adapter = DevicesAdapter()
        binding.rvMachineControlPanel.adapter = adapter
        binding.rvMachineControlPanel.layoutManager = LinearLayoutManager(activity)
        return RoomViewHolder(binding, adapter)
    }

    @SuppressLint("SetTextI18n")
    override fun onBindViewHolder(holder: RoomViewHolder, position: Int) {
        val currentRoom = roomList[position]
        Timber.d("Current room : $currentRoom")
        holder.apply {
            binding.tvRoom.text = "Room : ${currentRoom.room}"
            machinesAdapter.devicesLists(currentRoom.devices)
        }
    }

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

    override fun getItemCount(): Int {
        return roomList.size
    }
}
  • DevicesAdapter
class DevicesAdapter : RecyclerView.Adapter<DevicesAdapter.MachinesViewHolder>() {

    private var devicesList = emptyList<DevicesDataClass>()

    inner class MachinesViewHolder(val binding: ListItemControlPanelMachinesBinding) :
        RecyclerView.ViewHolder(binding.root)

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

    override fun onBindViewHolder(holder: MachinesViewHolder, position: Int) {
        val currentMachine = devicesList[position]
        holder.apply {
            binding.tvDevice.text = currentMachine.machine.toString()
        }
    }

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

    fun devicesLists(machine: List<DevicesDataClass>) {
        this.devicesList = machine
    }
}

Edit 1 : I need data on fragment as: Floor1 -> RoomA -> Device1, Device2 | RoomB -> Device3 || Floor2 -> RoomC -> Device4, Device5 | Room D -> Device6, Device 7

I am getting data as : Floor1 -> RoomA -> Device1, Device2, Device3, Device4 .... (upto last device) | RoomB -> Device1, Device2, Device3, Device4 .... (upto last device) || Floor2 -> Same

CodePudding user response:

There are a few problems with the code in loadRooms

  • There should not be outer lists for rooms or devices if those are unique per floor/rooms. You are making one deviceList and continually adding things to it instead of making a separate deviceList per room. Similarly you are making one roomList and adding to it instead of a separate list per floor.
  • You should not post the floor list inside the inner-most loop

It should look something like this:

fun loadRooms() {

    getAllFloors.observeForever(Observer {

        viewModelScope.launch(Dispatchers.Main) {

            val distinctFloorNames = it.distinct().sorted()
            val floorsList = mutableListOf<FloorsDataClass>()

            // Loop over floors
            for (floorName in distinctFloorNames) {

                // At *each* floor prepare a list of rooms
                val roomNames = repository.getAllRooms(floorName)
                val distinctRoomNames = roomNames.distinct().sorted()
                val roomsList = mutableListOf<RoomDataClass>

                // Loop over rooms in the floor
                for (roomName in distinctRoomNames) {

                    // In *each* room prepare a list of devices
                    val deviceNames = repository.getAllDevices(roomName)
                    val distinctDeviceNames = deviceNames.distinct().sorted()

                    // Transform the list of strings to a list of
                    // DeviceDataClass objects
                    val deviceData = distinctDeviceNames.map { dev -> DeviceDataClass(dev) }

                    // Add the device list to a room object
                    // and add the room to the room list
                    roomsList.add(RoomsDataClass(roomName, deviceData))
                }
                
                // Add the room list to a floor object and add
                // the floor to the floor list
                floorsList.add(FloorsDataClass(floorName, roomsList))
            }
            
            // Post the completed floor list
            floorListLiveData.postValue(floorsList)
            Timber.d("$floorsList")
        }
    })
}

modified so that the DevicesDataClass is a single string, not a list of strings (since the room already holds a list of devices)

data class DeviceDataClass(val machine: String) {}
  • Related