Step #4: Augmenting Our Motor

RosterMotor now needs to offer a way for the RosterListFragment to request a particular FilterMode.

First, add a filterMode property to RosterViewState:

data class RosterViewState(
  val items: List<ToDoModel> = listOf(),
  val isLoaded: Boolean = false,
  val filterMode: FilterMode = FilterMode.ALL
)

This will allow us to keep track of the currently-active filter mode, with an initial state of ALL.

Then, replace the current RosterMotor implementation with:

class RosterMotor(private val repo: ToDoRepository) : ViewModel() {
  private val _states = MutableStateFlow(RosterViewState())
  val states = _states.asStateFlow()
  private var job: Job? = null

  init {
    load(FilterMode.ALL)
  }

  fun load(filterMode: FilterMode) {
    job?.cancel()

    job = viewModelScope.launch {
      repo.items(filterMode).collect {
        _states.emit(RosterViewState(it, true, filterMode))
      }
    }
  }

  fun save(model: ToDoModel) {
    viewModelScope.launch {
      repo.save(model)
    }
  }
}

The save() function towards the bottom is unchanged from what we had before. The rest is quite different.

Before, states was very simple:

  val states = repo.items()
    .map { RosterViewState(it) }
    .stateIn(viewModelScope, SharingStarted.Eagerly, RosterViewState())

That is because we always loaded all of the to-do items. We still could have kept this code, but it would not give our UI the ability to change the filter mode, which is what we are trying to achieve.

However, if we later call repo.items(FilterMode.COMPLETED) or repo.items(FilterMode.OUTSTANDING), we get a different Flow than the one we had originally. That highlights a limitation of stateIn(): it can only give us one StateFlow. In our case, we may have several, as the user toggles between various filter options.

Moreover, it will simplify our RosterListFragment if there always is one StateFlow supplying the RosterViewState objects, rather than having to know to subscribe to different StateFlow objects at different times for different reasons. So, we have one or more Flow objects with our items, and we want to funnel them all into a single StateFlow of RosterViewState objects.

One solution for that is MutableStateFlow.

MutableStateFlow is a StateFlow that we manage ourselves. We supply an initial state in the constructor, then call emit() whenever our state changes.

So, what RosterMotor is doing is using a MutableStateFlow as the stable StateFlow that RosterListFragment observes.

With all that in mind…

However, we also track a Job object, representing our current Flow collection. On each load() call, we cancel() the preceding Job (if there was one), then save the launch() result as the next Job.

What happens if we fail to do this? Each items() call keeps getting collected, piling up if we call load() multiple times:

So, we track the Job from the last Flow collection and cancel() it before observing the next Flow.

And, if you run the app, it should work as it did before, showing you all of the to-do items in the list.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.