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:

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:

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.