In the first part, we discussed what a real Model is, the relationship between Model and state, and what Model can avoid common problems in Android development. In this article, we continue our exploration of “responsive APP development” by talking about model-view-Intent patterns for building responsive Android apps.

If you haven’t read part 1, you should read that first and then this. Let me briefly review the main points of the previous section: Let’s not write code like the following (the traditional MVP example)

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(a){
    getView().showLoading(true); // Displays a ProgressBar on the screen

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
        getView().showPersons(persons); // Displays a list of Persons on the screen
      }

      public void onError(Throwable error){
        getView().showError(error); // Displays a error message on the screen}}); }}Copy the code

We should create a “Model” that reflects “State “:

class PersonsModel {
  // Should be private in formal projects
  // We need to use the get method to get their values
  final boolean loading;
  final List<Person> persons;
  final Throwable error;

  public(boolean loading, List<Person> persons, Throwable error){
    this.loading = loading;
    this.persons = persons;
    this.error = error; }}Copy the code

Then the Presenter implementation looks something like this:

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(a){
    getView().render( new PersonsModel(true.null.null));// Displays the loading progress bar

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
        getView().render( new PersonsModel(false, persons, null));// Displays a list of people
      }

      public void onError(Throwable error){
          getView().render( new PersonsModel(false.null, error) ); // Error information is displayed}}); }}Copy the code

Now the View has a Model that renders the data to the UI by calling the Render (personsModel) method. In the previous article we also discussed the importance of one-way data flow, and that your business logic should drive your Model. Before we put everything together, let’s take a quick look at the general idea of MVI.

Model-View-Intent(MVI)

This pattern was developed by Andre Medeiros (Staltz) for a JavaScript framework he wrote called Cycle.js. Theoretically (mathematically), we can describe model-view-intent with the following expression:

  • Intent () : This function takes user input (for example, UI events, such as click events) and converts it into parameters accepted by the Model function. This parameter can be a simple String or some other complex structured data, such as Object. We can say that we change the Model with the intent of the intent().
  • Model () : The model() function takes the output of the Intent () function as input to manipulate the model. Its output is a new Model(because of the state change). Therefore, we should not update existing Models. Because we need the Model to be immutable! In the first part, I specifically used “counting APP” as a simple example to explain the importance of data invariance. Again, let’s not modify existing Model instances. We create a new model in the Model () method that changes based on the output of the intent. Note that the Model () method is the only place you can create new Model objects. Basically, we call the model() method our App’s business logic (Interactor, Usecase, Repository… Any patterns/terms you use in your application) and pass the new Model object as a result.
  • View () : This method receives the output of the Model () method. It then renders to the UI based on the output of model(). The view() method is roughly similar to view.render(model).

But we’re not building a responsive APP, are we? So, how does MVI make it “responsive”? What exactly does “responsive” mean? To answer the last question, “responsive” means that our app changes the UI depending on the state. In MVI, the “state” is represented by “Model”, essentially we expect our business logic to generate a new “Model” based on the user’s intent, and then change the new “Model” in the UI by calling the Render method of the View. This is the basic idea of MVI’s responsiveness.

Use RxJava to connect different points (where points refer to independent points of the Model,View, and Intent)

We want our data flow to be one-way. RxJava comes into play here. Do we have to use RxJava to build one-way data flow responsive apps or MVI mode apps? No, we can do it in other code. However, RxJava is great for event-based programming. Since the user interface is event-based, it makes sense to use RxJava.

In this series of blogs, we’re going to develop a simple e-commerce application. We make an HTTP request in the background to load the item we need to display. We can search for goods and add goods to shopping cart. All told, the App looks like this GIF:

github
In this series of blogs we use the “ViewState” tag to identify the Model
SearchViewState

public interface SearchViewState {

  /** * The search has not started */
  final class SearchNotStartedYet implements SearchViewState {}/** * load: wait to load */
  final class Loading implements SearchViewState {}/** * returns an empty result */
  final class EmptyResult implements SearchViewState {
    private final String searchQueryText;

    public EmptyResult(String searchQueryText) {
      this.searchQueryText = searchQueryText;
    }

    public String getSearchQueryText(a) {
      returnsearchQueryText; }}/** * verify the search results. Contains a list of items that match the search criteria. * /
  final class SearchResult implements SearchViewState {
    private final String searchQueryText;
    private final List<Product> result;

    public SearchResult(String searchQueryText, List<Product> result) {
      this.searchQueryText = searchQueryText;
      this.result = result;
    }

    public String getSearchQueryText(a) {
      return searchQueryText;
    }

    public List<Product> getResult(a) {
      returnresult; }}/** * indicates the error status in the search */
  final class Error implements SearchViewState {
    private final String searchQueryText;
    private final Throwable error;

    public Error(String searchQueryText, Throwable error) {
      this.searchQueryText = searchQueryText;
      this.error = error;
    }

    public String getSearchQueryText(a) {
      return searchQueryText;
    }

    public Throwable getError(a) {
      returnerror; }}}Copy the code

Java is a strongly typed language, and we need to choose a safe type for our Model. Our business logic returns SearchViewState. Of course, this definition is my personal preference. We can also define it in different ways, for example:

class SearchViewState {
  Throwable error; // if not null, an error has occurred
  boolean loading; // if true loading data is in progress
  List<Product> result; // if not null this is the result of the search
  boolean SearchNotStartedYet; // if true, we have the search not started yet
}
Copy the code

Again, you can define your Model however you want. If you can use Kotlin, sealed Classes is a good choice. Next, let me bring the focus back to business logic. Let’s take a look at how SearchInteractor, which is responsible for performing the search, does this. It was stated earlier that its “output” should be a SearchViewState object.

public class SearchInteractor {
  final SearchEngine searchEngine; // Make an HTTP request

  public Observable<SearchViewState> search(String searchString) {
    // Empty string, so no search
    if (searchString.isEmpty()) {
      return Observable.just(new SearchViewState.SearchNotStartedYet());
    }

    // Search for items
    return searchEngine.searchFor(searchString) // Observable<List<Product>>
        .map(products -> {
          if (products.isEmpty()) {
            return new SearchViewState.EmptyResult(searchString);
          } else {
            return new SearchViewState.SearchResult(searchString, products);
          }
        })
        .startWith(new SearchViewState.Loading())
        .onErrorReturn(error -> newSearchViewState.Error(searchString, error)); }}Copy the code

Let’s look at the method signature of searchinteractor.search () : We have a searchString as an input parameter and an Observable as an output. This already implies that we expect to launch any number of instances of SearchViewState on this observable stream over time. StartWith () is called before we start the query (via HTTP request). We fire searchViewState.loading in startWith. The goal is that when we click the search button, a progress bar will appear.

OnErrorReturn () catches all exceptions that occur during the search, and emits a searchViewState.error. Why don’t we just use onError callbacks when we subscribe to this Observable? This is a common misconception of RxJava: the onError callback means that our entire observation stream has entered an unrecoverable state, that is, the entire observation stream has been terminated. However, errors in our case, such as no network, are not unrecoverable errors. This is just another state (represented by Model). In addition, after that, we can move to other states. For example, once our network is reconnected, we can move to the “Loading state” represented by searchViewState.loading. So we set up a flow of observations from our business logic to the View, and our “state” changes with each launch of a changed Model. We certainly don’t want our stream of observations to end because of a network error. Therefore, such errors are handled as a state represented by Model (minus those fatal errors). In general, the Model observable is not terminated in MVI (onComplete () or onError () is never executed).

To summarize, SearchInteractor(business logic) provides an Observable that emits a new SearchViewState every time the state changes.

Next, let me discuss what the View layer looks like. What should the View layer do? Obviously, the View should display the Model. We have agreed that the View should have a method like Render (model). In addition, the View needs to provide a method for other layers to receive user-input events. These events are called intents in MVI. In this example, we only have one intent: the user can search for it by entering a string in the input field. A good practice in MVP is that we can define interfaces for views, so in MVI we can do the same.

public interface SearchView {

  /**
   * The search intent
   *
   * @return An observable emitting the search query text
   */
  Observable<String> searchIntent(a);

  /**
   * Renders the View
   *
   * @param viewState The current viewState state that should be displayed
   */
  void render(SearchViewState viewState);
}
Copy the code

In this case, our View provides only one intent, but in other business cases, multiple Intents may be required. In Part 1 we discussed why a single render() method is a good approach, and if you’re not sure why we need a single render(), you can read part 1 first. Before we implement the View layer, let’s take a look at what the final search page looks like

public class SearchFragment extends Fragment implements SearchView {

  @BindView(R.id.searchView) android.widget.SearchView searchView;
  @BindView(R.id.container) ViewGroup container;
  @BindView(R.id.loadingView) View loadingView;
  @BindView(R.id.errorView) TextView errorView;
  @BindView(R.id.recyclerView) RecyclerView recyclerView;
  @BindView(R.id.emptyView) View emptyView;
  private SearchAdapter adapter;

  @Override public Observable<String> searchIntent(a) {
    return RxSearchView.queryTextChanges(searchView) // Thanks Jake Wharton :)
        .filter(queryString -> queryString.length() > 3 || queryString.length() == 0)
        .debounce(500, TimeUnit.MILLISECONDS);
  }

  @Override public void render(SearchViewState viewState) {
    if (viewState instanceof SearchViewState.SearchNotStartedYet) {
      renderSearchNotStarted();
    } else if (viewState instanceof SearchViewState.Loading) {
      renderLoading();
    } else if (viewState instanceof SearchViewState.SearchResult) {
      renderResult(((SearchViewState.SearchResult) viewState).getResult());
    } else if (viewState instanceof SearchViewState.EmptyResult) {
      renderEmptyResult();
    } else if (viewState instanceof SearchViewState.Error) {
      renderError();
    } else {
      throw new IllegalArgumentException("Don't know how to render viewState "+ viewState); }}private void renderResult(List<Product> result) {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.VISIBLE);
    loadingView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    adapter.setProducts(result);
    adapter.notifyDataSetChanged();
  }

  private void renderSearchNotStarted(a) {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.GONE);
    errorView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
  }

  private void renderLoading(a) {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.VISIBLE);
    errorView.setVisibility(View.GONE);
    emptyView.setVisibility(View.GONE);
  }

  private void renderError(a) {
    TransitionManager.beginDelayedTransition(container);
    recyclerView.setVisibility(View.GONE);
    loadingView.setVisibility(View.GONE);
    errorView.setVisibility(View.VISIBLE);
    emptyView.setVisibility(View.GONE);
  }

  private void renderEmptyResult(a) { TransitionManager.beginDelayedTransition(container); recyclerView.setVisibility(View.GONE); loadingView.setVisibility(View.GONE); errorView.setVisibility(View.GONE); emptyView.setVisibility(View.VISIBLE); }}Copy the code

Render (SearchViewState) this method, we can see what it does by looking at it. In the searchIntent() method we use Jake Wharton’s RxBindings library, which enables RxJava to bind Android UI controls like observables. Rxsearchview.querytext () creates an Observable object that emits the search string each time the user enters some character in the EditText. We use filter() to ensure that the search starts only when the user enters more than three characters. Also, rather than requesting the network every time the user types a new character, we request the network after the user has finished typing (debounce() stays for 500 milliseconds to determine if the user is done typing).

So we know that for this page, the input is searchIntent() and the output is render(). How do we go from “input” to “output”? The following video visualizes this process:

flatMap()
Presenter

public class SearchPresenter extends MviBasePresenter<SearchView.SearchViewState> {
  private final SearchInteractor searchInteractor;

  @Override protected void bindIntents(a) {
    Observable<SearchViewState> search =
        intent(SearchView::searchIntent)
            .switchMap(searchInteractor::search) // I used flatMap() in the video above, but switchMap() is more applicable here.observeOn(AndroidSchedulers.mainThread()); subscribeViewState(search, SearchView::render); }}Copy the code

What is MviBasePresenter? This is a library I wrote called Mosby (Mosby3.0 has added an MVI component). This blog was not written to introduce Mosby, but I wanted to give a brief introduction to MviBasePresenter. Describe how MviBasePresenter makes it easy for you to use. There is no dark magic in this library. Let’s start with lifecycle :MviBasePresenter doesn’t actually have a Lifecyle. A bindIntent() method binds the intent of the view to the business logic. Typically, you use flatMap() or switchMap or concatMap() to pass intents to business logic. The call to this method is only the first time the View is attached to Presenter. It will not be called when the View is re-attached to Presenter (for example, when the screen orientation changes).

This may sound strange, but one might say, “MviBasePresenter can hold even when the screen orientation changes? If so, how does Mosby ensure that the observable stream’s data is in memory and not lost?” And that’s the intent() and subscribeViewState() that answer that question. Intent () creates a PublishSubject internally and uses it as a “portal” for your business logic. So the PublishSubject actually subscribes to the Intent Observable of the View. Calling the Intent (o1) actually returns a PublishSubject subscribed to o1.

Mosby separates the View from Presenter when the direction changes, but only temporarily unsubscribes the internal PublishSubject. Also, when the View reconnects to the Presenter, the PublishSubject re-subscribes to the Intent of the View.

SubscribeViewState () does the same thing in a different way (Presenter to View communication). It creates a BehaviorSubject internally that acts as a “portal” for the business logic to the View. Since it is a BahaviorSubject, we can receive a “model update” message from the business logic, even if no view is currently attached (for example, the view is on the return stack). The BehaviorSubjects always retain the last-minute value, and whenever a View is attached to it, it starts receiving again or passing its reserved value to the View.

The rule is simple: “wrap” the intent of all views (click events, etc.) with intent(). SubscribeViewState () instead of Observable.subscribe (…) .

The counterpart of the bindIntent() is the unbindIntents() method, which is called only once, and when the unbindIntents() is called, the View is destroyed permanently. For example, putting fragments on the return stack does not permanently destroy the view, but if an Activity ends its life cycle, it permanently destroys the view. Since the Intent () and subscribeViewState () are already responsible for subscription management, you hardly need to implement unbindIntents ().

So what about onPause() and onResume() in our life cycle? I think Presenters don’t need to pay attention to the life cycle. If, for example, you want to handle the life cycle in a Presenter, for example, you want onPause() as an intent. Your View needs to provide a pauseIntent() method, which is triggered by the lifecycle, not the user interaction, but both are valid intents.

conclusion

In Part 2, we discussed the basics of Model-view-Intent and implemented a simple search page with MVI. Let’s get started. Maybe this example is too simple. You can’t see the advantage of MVI, where Model stands for state and one-way data flow as well as traditional MVP or MVVM. MVVM and MVVM are good. MVI may not be as good as them. Even so, I think MVI helps us write elegant code when faced with complex problems. We will discuss state reduction in part 3 of this blog series.