The Controller

Given the actions and the repositories, the controller is the glue code, updating the repositories based on the actions and emitting results to trigger updates to the view state and, from there, the UI.

Subscribing to Actions

To get the actions over to the Controller, the RosterViewModel has an RxJava PublishSubject that serves as its Observable source of a stream of Action objects. Every time process() is called, the RosterViewModel emits that Action onto the actionSubject:

  public void process(Action action) {
    actionSubject.onNext(action);
  }

As part of its constructor, the RosterViewModel creates a Controller. Once again, this may not be an appropriate approach for a more complex app, where there may be multiple view-models needing to work with a common Controller, but it suffices for here. Along the way, the RosterViewModel calls a subscribeToActions() method, so that the Controller can subscribe to those Action events:

  public void subscribeToActions(Observable<Action> actionStream) {
    actionStream
      .observeOn(Schedulers.single())
      .subscribe(this::processImpl);
  }

This particular subscription routes the work to a single() thread, to keep the repository work off of the main application thread. And, it passes the Action objects to a processImpl() method.

Doing the Work and Publishing Results

So far, the only place where we care about specific types of Action is when we publish them. In effect, we take several event types and combine them into a single type for convenience. However, at some point, we need to split them back out again, so we can handle specific logic for specific actions, and that occurs in processImpl():

  private void processImpl(Action action) {
    if (action instanceof Action.Add) {
      add(((Action.Add)action).model());
    }
    else if (action instanceof Action.Edit) {
      modify(((Action.Edit)action).model());
    }
    else if (action instanceof Action.Delete) {
      delete(((Action.Delete)action).models());
    }
    else if (action instanceof Action.Load) {
      load();
    }
    else if (action instanceof Action.Select) {
      select(((Action.Select)action).position());
    }
    else if (action instanceof Action.Unselect) {
      unselect(((Action.Unselect)action).position());
    }
    else if (action instanceof Action.UnselectAll) {
      unselectAll();
    }
    else if (action instanceof Action.Show) {
      show(((Action.Show)action).current());
    }
    else if (action instanceof Action.Filter) {
      filter(((Action.Filter)action).filterMode());
    }
    else {
      throw new IllegalStateException("Unexpected action: "+action.toString());
    }
  }

This basically “unwraps” the action and invokes a dedicated method per action type. Most of those methods work with one of the repositories for the data associated with that action. All of these methods use a BehaviorSubject named resultSubject to publish the result, and we will examine that in detail a bit later.

The action method can be broken down into four groups, based on the data being operated on and the operation type.

add()/modify()/delete()

These three methods are fairly straightforward. They call the associated method on the ToDoRepository and publish their results:

  private void add(ToDoModel model) {
    toDoRepo.add(model);
    resultSubject.onNext(Result.added(model));
  }

  private void modify(ToDoModel model) {
    toDoRepo.replace(model);
    resultSubject.onNext(Result.modified(model));
  }

  private void delete(List<ToDoModel> toDelete) {
    toDoRepo.delete(toDelete);
    resultSubject.onNext(Result.deleted(toDelete));
  }

filter()

filter() is similar, except that it works with the FilterModeRepository:

  private void filter(FilterMode mode) {
    filterModeRepo.save(mode);
    resultSubject.onNext(Result.filter(mode));
  }

select()/unselect()/unselectAll()

The selections are not persistent — they are purely a UI contrivance. However, they are part of the view state, and the only way to update the view state is by going through the action-controller-reducer flow. So, these three methods just publish results to get their data updates over to the reducer:

  private void select(int position) {
    resultSubject.onNext(Result.selected(position));
  }

  private void unselect(int position) {
    resultSubject.onNext(Result.unselected(position));
  }

  private void unselectAll() {
    resultSubject.onNext(Result.unselectedAll());
  }

load()

load() is the quirky one, as this would not be traditional computer programming if everything were simple.

  private void load() {
    Single<Result> loader=
      Single.zip(toDoRepo.all(), filterModeRepo.load(ctxt),
        (models, mode) -> (Result.loaded(models, mode)));

    loader
      .subscribeOn(Schedulers.single())
      .subscribe(resultSubject::onNext);
  }

The load action is to load our data. In our case, though, we have data from two repositories: the ToDoRepository and the FilterModeRepository. Each publishes a Single for loading their particular bits of data. We need to publish results once both of those Single objects have completed processing.

RxJava’s zip() operator — which has nothing much to do with ZIP files, zip ties, or ziplines — is designed for this sort of scenario. You give zip() multiple observables, and it invokes your code for each set of results emitted by the source observables. Since our observables are Single, they only emit one object, and so we get control once both Single results are in. We then publish those as our own result.


Prev Table of Contents Next

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