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…
-
_statesis ourMutableStateFlow, and it isprivate - Since we do not want
RosterListFragmentto know the implementation details,statesis a plainStateFlowproperty that happens to point to theMutableStateFlowin_states -
load()will observe a call toitems()on therepo, supplying whatever the requestedFilterModeis, andemit()any changes as newRosterViewStateobjects through_states -
init {}triggers our initialload()call, to retrieveALLitems
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:
- We call
load()first when theRosterMotoris created, via theinit {}block, and we start observing aFlowfromitems(). - Later, when we add logic to
RosterListFragmentto switch filters, we will callload()again for a new filter. So, we start observing aFlowfromitems(). However, if we did not cancel theJobfrom the firstload()call, thatFlowwill continue to be collected. - The user goes and adds another to-do item, and both
Flowobjects report the revised database contents, because Room does not know that theseFlowobjects are, in effect, duplicates. So, weemit()twoRosterViewStateobjects, one triggered by eachFlow. - And, if the user toggles the filter mode several times, we might pile up several outstanding
Flowobjects.
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.