The Reducer in the RosterViewModel
What remains is the reducer: accepting the results and updating the view state to match. In this sample app, that is part of the role of the RosterViewModel
.
You might wonder why this is called “RosterViewModel
”, given that it has responsibilities that do not exactly line up with a classic view-model. The name comes from the base class: AndroidViewModel
. We want to retain this object across configuration changes, and the way to do that with the Architecture Components is to use ViewModel
, AndroidViewModel
, ViewModelProviders
, and so forth. In the author’s opinion, Google would have been better served naming their system something that did not have “ViewModel
” in it, just as Room does not have “Model
” or “Repository
” in it.
Subscribing to Results
Our Controller
publishes Result
objects via the resultSubject
Observable
, exposed via a resultStream()
method. RosterViewModel
needs to subscribe to that stream, take the results, fold them into the view state, and publish an updated view state.
That is wired together in the RosterViewModel
constructor:
public RosterViewModel(Application ctxt) {
super(ctxt);
ObservableTransformer<Result, ViewState> toView=
results -> (results.map(result -> {
lastState=foldResultIntoState(lastState, result);
return(lastState);
}));
Controller controller=new Controller(ctxt);
states=LiveDataReactiveStreams
.fromPublisher(controller.resultStream()
.subscribeOn(Schedulers.single())
.compose(toView)
.cache()
.toFlowable(BackpressureStrategy.LATEST)
.share());
controller.subscribeToActions(actionSubject);
process(Action.load());
}
The stateStream()
method that our views use to get the updated live states is a LiveData
, held onto as a states
field and exposed via stateStream()
. To create states
, we:
- Get the
resultStream()
Observable
from theController
- Arrange to process those
Result
objects on a background thread - Use the
toView
ObservableTransformer
to convertResult
objects into a newViewState
— we will examine this part in greater detail shortly - Cache the resulting
ViewState
- Convert the
Observable
to aFlowable
, only worrying about the latestViewState
that we receive -
share()
thatFlowable
among multiple subscribers - Convert that
Flowable
into aLiveData
usingLiveDataReactiveStreams.fromPublisher()
Merging Results Into the ViewState
An ObservableTransformer
is simply a way of packaging an RxJava operator or chain of operators into a separate object. That can be useful in cases where:
- You might want to reuse the same operator(s) in multiple chains
- The operator might be fairly complex, and so you want to pull it out of the chain declaration to keep the chain itself more readable
So, let’s look at that operator more closely:
ObservableTransformer<Result, ViewState> toView=
results -> (results.map(result -> {
lastState=foldResultIntoState(lastState, result);
return(lastState);
}));
We get in our Result
stream, and our declaration says that we are emitting a ViewState
. We are using the map()
operator to make that conversion, where the bulk of the logic lies in a foldResultIntoState()
method.
What we are trying to do is to mix a new Result
with the previous ViewState
to get a new ViewState
. This implies that we have the previous ViewState
somewhere. That is the lastState
field, initialized to be an empty ViewState
at the outset:
private ViewState lastState=ViewState.empty().build();
It is the job of foldResultIntoState()
to create the new ViewState
, which the ObservableTransformer
both holds in lastState
and returns to flow through the rest of the chain.
foldResultIntoState()
needs to identify the specific type of Result
(e.g., we added an item, we deleted an item) and update the ViewState
. foldResultIntoState()
mostly handles the first part: identifying the specific type of Result
:
private ViewState foldResultIntoState(@NonNull ViewState state,
@NonNull Result result) throws Exception {
if (result instanceof Result.Added) {
return(state.add(((Result.Added)result).model()));
}
else if (result instanceof Result.Modified) {
return(state.modify(((Result.Modified)result).model()));
}
else if (result instanceof Result.Deleted) {
return(state.delete(((Result.Deleted)result).models()));
}
else if (result instanceof Result.Loaded) {
List<ToDoModel> models=((Result.Loaded)result).models();
return(ViewState.builder()
.isLoaded(true)
.items(models)
.filterMode(((Result.Loaded)result).filterMode())
.current(models.size()==0 ? null : models.get(0))
.build());
}
else if (result instanceof Result.Selected) {
return(state.selected(((Result.Selected)result).position()));
}
else if (result instanceof Result.Unselected) {
return(state.unselected(((Result.Unselected)result).position()));
}
else if (result instanceof Result.UnselectedAll) {
return(state.unselectedAll());
}
else if (result instanceof Result.Showed) {
return(state.show(((Result.Showed)result).current()));
}
else if (result instanceof Result.Filter) {
return(state.filtered(((Result.Filter)result).filterMode()));
}
else {
throw new IllegalStateException("Unexpected result type: "+result.toString());
}
}
In the case of Result.Loaded
, we are creating a brand-new ViewState
from scratch. We only get this event when we first load the data, and so there is no meaningful prior state to use. In all the other scenarios, we call mutation methods on the existing ViewState
, which turn around and create a new ViewState
with the requested changes applied:
ViewState add(ToDoModel model) {
List<ToDoModel> models=new ArrayList<>(items());
models.add(model);
sort(models);
return(toBuilder()
.items(Collections.unmodifiableList(models))
.current(model)
.build());
}
ViewState modify(ToDoModel model) {
List<ToDoModel> models=new ArrayList<>(items());
ToDoModel original=find(models, model.id());
if (original!=null) {
int index=models.indexOf(original);
models.set(index, model);
}
sort(models);
return(toBuilder()
.items(Collections.unmodifiableList(models))
.build());
}
ViewState delete(List<ToDoModel> toDelete) {
List<ToDoModel> models=new ArrayList<>(items());
for (ToDoModel model : toDelete) {
ToDoModel original=find(models, model.id());
if (original==null) {
throw new IllegalArgumentException("Cannot find model to delete: "+model.toString());
}
else {
models.remove(original);
}
}
sort(models);
return(toBuilder()
.items(Collections.unmodifiableList(models))
.build());
}
ViewState selected(int position) {
HashSet<Integer> selections=new HashSet<>(selections());
selections.add(position);
return(toBuilder()
.selections(Collections.unmodifiableSet(selections))
.build());
}
ViewState unselected(int position) {
HashSet<Integer> selections=new HashSet<>(selections());
selections.remove(position);
return(toBuilder()
.selections(Collections.unmodifiableSet(selections))
.build());
}
ViewState unselectedAll() {
return(toBuilder()
.selections(Collections.unmodifiableSet(new HashSet<>()))
.build());
}
ViewState show(ToDoModel current) {
return(toBuilder()
.current(current)
.build());
}
ViewState filtered(FilterMode mode) {
return(toBuilder()
.filterMode(mode)
.build());
}
In all cases, we get a new immutable ViewState
, which then flows out of the RosterViewModel
to the view layer, so the fragments can update their UI as needed.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.