Search code examples
androidandroid-recyclerviewandroid-roomandroid-database

Data imported from Database is not set in view


enter image description here

I'm making a screen similar to the image.

The data set in advance is taken from the Room DB and the data is set for each tab.

Each tab is a fragment and displays the data in a RecyclerView.

Each tab contains different data, so i set Tab to LiveData in ViewModel and observe it.

Therefore, whenever tabs change, the goal is to get the data for each tab from the database and set it in the RecyclerView.

However, even if I import the data, it is not set in RecyclerView.

I think the data comes in well even when I debug it.

This is not an adapter issue.

What am I missing?


WorkoutList

@Entity
data class WorkoutList(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val chest: List<String>,
    val back: List<String>,
    val leg: List<String>,
    val shoulder: List<String>,
    val biceps: List<String>,
    val triceps: List<String>,
    val abs: List<String>
)

ViewModel

class WorkoutListViewModel(application: Application) : AndroidViewModel(application){
    private var _part :MutableLiveData<BodyPart> = MutableLiveData()
    private var result : List<String> = listOf()

    private val workoutDao = WorkoutListDatabase.getDatabase(application).workoutListDao()
    private val workoutListRepo = WorkoutListRepository(workoutDao)
    
    val part = _part

    fun setList(part : BodyPart) : List<String> {
        _part.value = part

        viewModelScope.launch(Dispatchers.IO){
            result = workoutListRepo.getWorkoutList(part)
        }
        return result
    }
}

Repository

class WorkoutListRepository(private val workoutListDao: WorkoutListDao) {
    suspend fun getWorkoutList(part: BodyPart) : List<String> {
        val partList = workoutListDao.getWorkoutList()

        return when(part) {
            is BodyPart.Chest -> partList.chest
            is BodyPart.Back -> partList.back
            is BodyPart.Leg -> partList.leg
            is BodyPart.Shoulder -> partList.shoulder
            is BodyPart.Biceps -> partList.biceps
            is BodyPart.Triceps -> partList.triceps
            is BodyPart.Abs -> partList.abs
        }
    }
}

Fragment

class WorkoutListTabPageFragment : Fragment() {
    private var _binding : FragmentWorkoutListTabPageBinding? = null
    private val binding get() = _binding!!
    private lateinit var adapter: WorkoutListAdapter
    private lateinit var part: BodyPart

    private val viewModel: WorkoutListViewModel by viewModels()

    companion object {
        @JvmStatic
        fun newInstance(part: BodyPart) =
            WorkoutListTabPageFragment().apply {
                arguments = Bundle().apply {
                    putParcelable("part", part)
                }
            }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let { bundle ->
            part = bundle.getParcelable("part") ?: throw NullPointerException("No BodyPart Object")
        }
    }

    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        _binding = FragmentWorkoutListTabPageBinding.inflate(inflater, container, false)
        binding.apply {
            adapter = WorkoutListAdapter()
            rv.adapter = adapter
        }

        val result = viewModel.setList(part)

        // Set data whenever tab changes
        viewModel.part.observe(viewLifecycleOwner) { _ ->
//            val result = viewModel.setList(part)
            adapter.addItems(result)
        }

        return binding.root
    }
}     viewModel.part.observe(viewLifecycleOwner) { _ ->
            adapter.addItems(result)
        }

        return binding.root
    }
}

Solution

  • The problem you are seeing is that in setList you start an asynchronous coroutine on the IO thread to get the list, but then you don't actually wait for that coroutine to run but just return the empty list immediately.

    One way to fix that would be to observe a LiveData object containing the list, instead of observing the part. Then, when the asynchronous task is complete you can post the retrieved data to that LiveData. That would look like this in the view model

    class WorkoutListViewModel(application: Application) : AndroidViewModel(application) {
    
        private val _list = MutableLiveData<List<String>>()
        val list: LiveData<List<String>>
            get() = _list
    
        // "part" does not need to be a member of the view model 
        // based on the code you shared, but if you wanted it
        // to be you could do it like this, then
        // call "viewModel.part = part" in "onCreateView". It does not need
        // to be LiveData if it's only ever set from the Fragment directly.
        //var part: BodyPart = BodyPart.Chest
        
           
        // calling getList STARTS the async process, but the function
        // does not return anything
        fun getList(part: BodyPart) {
            viewModelScope.launch(Dispatchers.IO){
                val result = workoutListRepo.getWorkoutList(part)
                _list.postValue(result)
            }
        }
    }
    

    Then in the fragment onCreateView you observe the list, and when the values change you add them to the adapter. If the values may change several times you may need to clear the adapter before adding the items inside the observer.

    
    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        //...
    
        // Set data whenever new data is posted
        viewModel.list.observe(viewLifecycleOwner) { result ->
            adapter.addItems(result)
        }
        
        // Start the async process of retrieving the list, when retrieved
        // it will be posted to the live data and trigger the observer
        viewModel.getList(part)
    
        return binding.root
    }
        
    

    Note: The documentation currently recommends only inflating views in onCreateView and doing all other setup and initialization in onViewCreated - I kept it how you had it in your question for consistency.