Step #3: Consuming Roster View States
Our RosterListFragment now will complain that it no longer has a getItems() function on our RosterMotor. Specifically, we have an error on this line in onViewCreated():
adapter.submitList(motor.getItems())
Replace that line with:
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
motor.states.collect { state ->
adapter.submitList(state.items)
binding?.apply {
when {
state.items.isEmpty() -> {
empty.visibility = View.VISIBLE
empty.setText(R.string.msg_empty)
}
else -> empty.visibility = View.GONE
}
}
}
}
We start off by referencing viewLifecycleOwner.lifecycleScope. This gives us a CoroutineScope tied to the lifecycle of this fragment’s views. Whereas a viewModelScope of a ViewModel will cancel its outstanding coroutines once the ViewModel is cleared, the CoroutineScope that we get with viewLifecycleOwner.lifecycleScope is one that will cancel its outstanding coroutines once the fragment’s views are destroyed (i.e., when the fragment is called with onDestroyView()).
The specific subtype of CoroutineScope that we get from viewLifecycleOwner.lifecycleScope is a LifecyleCoroutineScope, and it has some functions that give us sub-scopes based on specific lifecycle events. In this case, we use launchWhenStarted(). This returns a CoroutineScope that will:
- Start running the supplied lambda expression as a coroutine once the fragment’s views are visible
- Suspend running that coroutine once the fragment’s views are no longer visible
- Resumes running that coroutine if the fragment is restarted and its views become visible again
- Cancels the coroutine if the fragment’s views are destroyed
The net of all of that is we have a CoroutineScope that does work during useful times (when the views are visible) and cleans up when our views are destroyed.
Inside that scope, we call motor.states.collect(). states is our StateFlow from our RosterMotor (motor). collect() observes the states emitted by the StateFlow for as long as the coroutine runs. Our supplied lambda expression gets called for each such state, including the initial empty state. So, the state parameter in the lambda expression is our RosterViewState. However, that view-state not only represents the initial list of to-do items, but any changed editions of the list that are published by the ToDoRepository — we keep getting view-states pushed to us as the data changes, as Room gives updates to the repository, which gives them to the RosterMotor, which in turn streams them to the fragment.
So, our lambda expression can update the RosterAdapter and the empty visibility, both for our initial state and after we make any changes to that state. That is why if you run the app after making these changes, you will see that the items that you add, edit, and delete have those actions reflected in the list.
Note that the collect() function used in motor.states.collect() is an extension function that needs to be imported:
import kotlinx.coroutines.flow.collect
Also, in onViewCreated() of RosterListFragment, replace:
binding?.empty?.visibility =
if (adapter.itemCount == 0) View.VISIBLE else View.GONE
with:
binding?.empty?.visibility = View.GONE
We are now making the empty state visible inside our when() for the view-states, so we do not need to be manipulating it here anymore.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.