0

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
    }
}
7
  • 1
    Check this question and answers - your coroutine in setList is asynchronous and the function returns the empty list before the coroutine actually runs to populate it. Try making the list a LiveData, posting the db data to it in the coroutine instead of returning the list, and observing it and populating the adapter in the observer. Commented Jun 2, 2022 at 23:24
  • 1
    That is correct, it returns before the coroutine runs. You may need LiveData and observers for both things, part and list, or just one. There are many ways to do it, you just can't return the list edited in the coroutine like you are currently. Commented Jun 3, 2022 at 22:03
  • 1
    You could also make setList a suspend function and call it from a coroutine in the part observer. Commented Jun 3, 2022 at 23:21
  • 1
    Also (last note, I promise) having part as both live data in the view model and as a class member in the fragment will probably lead to trouble with inconsistent state. The view model should hold the single source of truth/state. When you read the part from the bundle you should pass that to the view model instead of saving it as a class member. Commented Jun 4, 2022 at 1:36
  • 1
    I added an answer showing one approach. The top answers on that linked question are all about callbacks, but if you check the later answers there are more relevant solutions. Commented Jun 5, 2022 at 21:29

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you so much. Thanks I solved the problem. Callbacks seem to be an easy concept, but also difficult.. It's not easy to handle yet. And you said to put only View's inflation in onCreateView. But do you also move code like binding.apply { } to onCreatedView?
No, I meant stuff like setting up the observer and calling getList

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.