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…
-
_states
is ourMutableStateFlow
, and it isprivate
- Since we do not want
RosterListFragment
to know the implementation details,states
is a plainStateFlow
property that happens to point to theMutableStateFlow
in_states
-
load()
will observe a call toitems()
on therepo
, supplying whatever the requestedFilterMode
is, andemit()
any changes as newRosterViewState
objects through_states
-
init {}
triggers our initialload()
call, to retrieveALL
items
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 theRosterMotor
is created, via theinit {}
block, and we start observing aFlow
fromitems()
. - Later, when we add logic to
RosterListFragment
to switch filters, we will callload()
again for a new filter. So, we start observing aFlow
fromitems()
. However, if we did not cancel theJob
from the firstload()
call, thatFlow
will continue to be collected. - The user goes and adds another to-do item, and both
Flow
objects report the revised database contents, because Room does not know that theseFlow
objects are, in effect, duplicates. So, weemit()
twoRosterViewState
objects, one triggered by eachFlow
. - And, if the user toggles the filter mode several times, we might pile up several outstanding
Flow
objects.
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.