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.