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:

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.