REACTIVE APPS WITH Model-view-Intent-Part1-model by Hannes Dorfmann

One day, I suddenly realized that my definition of the Model layer was all wrong, and after updating my knowledge, I found that all the confusion or headaches I had been having in my Android theme discussions disappeared.

As a result, I chose to build a responsive APP using RxJava and Model-view-Intent (MVI), which I had never tried before. Although the APP I had developed before was also responsive, the performance of responsive programming was nothing like this. You’ll see that in the next series of articles. But to start this series, I’d like to make a point:

The so-calledModelWhat exactly is the layer that I was right aboutModelWhat’s wrong with the layer definition?

Why do I say I have the wrong understanding and use of the Model layer? Of course, there are many architectural patterns that separate the View layer from the Model layer, at least in the Android development world, The most famous are model-View-Controller (MVC), Model-View-Presenter (MVP), and Model-View-ViewModel (MVVM) — have you noticed? The Model was integral to all of these architectural patterns, but I realized that in most cases I didn’t have a Model at all.

For example, in a simple case of pulling the Person list from the back end, a traditional MVP implementation would look like this:

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(a){
    getView().showLoading(true); // Show a ProgressBar

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
        getView().showPersons(persons); // Display a list of users
      }

      public void onError(Throwable error){
        getView().showError(error); // Display error information}}); }}Copy the code

But what exactly does Model mean in this code? Do you mean the backend network request? No, that’s just business logic. Is it a list of users requesting results? No, like the ProgressBar and error message display, it only represents a small part of what the View layer can show.

So what exactly does the Model layer mean?

From my personal understanding, the Model class should be defined like this:

class PersonsModel {
  // In a real project, the need is defined as private
  And we need to access them through getters and setters
  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

The Presenter layer should implement this:

class PersonsPresenter extends Presenter<PersonsView> {

  public void load(a){
    getView().render( new PersonsModel(true.null.null));// Show a ProgressBar

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

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

The View layer now holds a Model and can go through it to rendered controls on the screen. This is not a new concept. Trygve Reenskaug had a similar concept in his original MVC definition back in 1979: View observes Model changes.

However, the term MVC is used to describe too many different patterns that are not exactly the same as the one Reenskaug formulated in 1979. For example, backend developers use MVC framework, iOS has ViewController, how does that define MVC in Android? Is an Activity a Controller? So what about ClickListener? Today, the term MVC has become a big misunderstanding that misunderstands and uses what Reenskaug originally formulated — the topic stops there and the whole article gets out of hand.

Without further ado, Model ownership will solve many of the problems we often encounter in Android development:

  • 1. Status problems
  • 2. Screen orientation changes
  • 3. Navigate through the page stack
  • 4. The process is terminated
  • 5. Invariance of one-way data flow
  • 6. Debugable and reproducible states
  • 7. Testable

To discuss these key issues, let’s first look at how “traditional” MVVM and MVVM implementations handle them, and then talk about how the Model bypasses these common pitfalls.

1. Status problems

Responsive apps, it’s a very popular topic these days, isn’t it? A responsive App is a UI that responds to changes in state, and there’s a great word in that sentence: state. What is a state? Most of the time, we describe state as what we see on the screen, such as the Loading State when the ProgressBar is displayed.

Crucially, we front-end developers tend to focus on the UI. This is not necessarily a bad thing, because a good UI experience determines whether users will use your product and therefore whether it will be successful. But looking at the MVP sample code above (not the one using PersonModel), the UI state is coordinated by the Presenter, who tells the View layer how to present it.

As with MVVM, I want to distinguish between two implementations of MVVM in this article: the first relies on the DataBinding library and the second relies on RxJava; For the DataBinding dependent former, its state is defined directly in the ViewModel:

class PersonsViewModel {
  ObservableBoolean loading;
  / / to omit...

  public void load(a){

    loading.set(true);

    backend.loadPersons(new Callback(){
      public void onSuccess(List<Person> persons){
      loading.set(false);
      // Omit other code, such as rendering persons
      }

      public void onError(Throwable error){
        loading.set(false);
        // Omit other code, such as displaying error messages}}); }}Copy the code

The RxJava implementation of MVVM does not rely on the DataBinding engine. Instead, it binds Observable to UI controls, such as:

class RxPersonsViewModel {
  private PublishSubject<Boolean> loading;
  private PublishSubject<List<Person> persons;
  private PublishSubject loadPersonsCommand;

  public RxPersonsViewModel(a){
    loadPersonsCommand.flatMap(ignored -> backend.loadPersons())
      .doOnSubscribe(ignored -> loading.onNext(true))
      .doOnTerminate(ignored -> loading.onNext(false))
      .subscribe(persons)
      // There is no unique implementation
  }

  // Subscribe to it in the View layer (e.g. Activity/Fragment)
  public Observable<Boolean> loading(a){
    return loading;
  }

  // Subscribe to it in the View layer (e.g. Activity/Fragment)
  public Observable<List<Person>> persons(){
    return persons;
  }

  // Whenever this operation is triggered (onNext()), load Persons data
  public PublishSubject loadPersonsCommand(a){
    returnloadPersonsCommand; }}Copy the code

Of course, this code isn’t perfect, and you might implement it differently; I want to point out that in most MVP or MVVM states are driven by ViewModel or Presenter.

This leads to the following:

  • The business logic has its own state, the Presenter (or ViewModel) has its own state (and you need code to synchronize their state), and the View may have its own state (for example, Call the View’s setVisibility() method to set its visibility, or the Android system will restore its state from the bundle when it recreates.

  • 2. It’s ok for a Presenter (or ViewModel) to have any number of inputs (the View layer triggers the behavior and gives it to the Presenter to handle), but the Presenter also has many outputs (or output channels in the MVP case, Such as the showLoading () or the showError (); In MVVM, the ViewModel implementation also provides multiple Observables, which ultimately leads to state conflicts in the View layer, Presenter layer, and business logic, especially when dealing with multiple threads.

On a good day, this only results in visual errors, such as displaying both load indicators (” load state “) and error indicators (” error state “), as shown below:

In the worst case, you receive a serious error report from a crash reporting tool such as Crashlytics, but you cannot reproduce the error, so there is little you can do to fix it.

What if, from the bottom layer (the business logic layer) to the top layer (the UI view layer), there was one and only one source that actually described the state? In fact, we have already demonstrated similar concepts through examples at the beginning of this article when we talked about the Model:

class PersonsModel {
  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

Guess what? Model maps state, and when I figured that out, many state-related problems were solved (and avoided even before coding); The Presenter layer now has only one output:

getView().render(PersonsModel)

It corresponds to a mathematically simple function, such as f(x) = y, or f(a,b,c) for a function with multiple inputs, but it’s also an output.

Mathematics isn’t all tea for everyone, just as mathematicians don’t know what bugs are — but software engineers need to taste them.

It’s important to know what a Model is and how to build one, because ultimately the Model will solve the state problem.

2. Screen orientation changes

The issue of state backtracking after screen rotation is already handled by Google’s official ViewModel component, so developers don’t have to worry about it anymore, but this section is still worth reading.

Screen rotation on Android devices is a challenging enough problem; The simplest solution is to ignore it and reload the data every time the screen rotates. This works, and most of the time, your APP also works offline and its data comes from a database or other local cache, which means that data loads quickly after the screen is rotated.

But, personally, I don’t like seeing loading boxes, even in milliseconds, because I don’t think it’s a perfect user experience, so people (including me) have started using MVP, which includes a retention Presenter, which separates and destroys the View layer as the screen rotates. The Presenter is kept in memory without being destroyed, and then the View layer connects to the Presenter again.

MVVM using RxJava can implement the same concept, but keep in mind that once the View unsubscribes to the ViewModel, the observable stream is destroyed. You can solve this problem with Subject; For MVVMS built by DataBinding, the ViewModel is directly bound to the View layer by DataBinding, so to avoid memory leaks, we need to destroy the ViewModel when the screen rotates.

The question for retained Presenters or viewModels is: how do we retrace the state of the View after the screen rotation, so that the View and Presenter come back to the same state again? I wrote an MVP library called Mosby that contains a feature called ViewState, which basically synchronizes the state of the business logic with View. Moxy, another MVP library, comes up with a very interesting solution — to recreate the state of the View after the screen orientation changes by using Commands:

I’m pretty sure there are other solutions to the View layer state problem. Let’s step back and generalize about the problem these libraries are trying to solve: it’s the state problem we’ve already discussed.

Again, we solve this problem with a Model that reflects the current state and a render Model method, as simple as calling getView().render(PersonsModel).

3. Navigate through the page stack

Is it necessary to keep presenters (or ViewModels) when views are no longer in use? For example, if the user jumps to another interface, the Fragment(View) is replaced by another Fragment, so Presenter is no longer held by any View.

If the View layer is not associated with the Presenter, the Presenter will not be able to reflect the latest data on the View according to the business logic. But what if the user comes back (for example by pressing the back button), reload the data or reuse the existing Presenter? It seems like a philosophical question.

Usually, once the user returns to the previous interface, he expects to return to the previous interface and continue. This is still like the View layer state restoration problem in section 2, and the solution is succinct: when the user returns, we get the Model representing the state, and then simply call getView().render(PersonsModel) to render the View layer.

4. The process is terminated

Process terminations are a bad thing, and we need to rely on libraries to help us recover state after process terminations — I think this is a common misconception in Android development.

First, there is only one reason for a process to die, and it’s a good enough reason — the Android operating system needs more resources for other applications or to save batteries. This will never happen if your APP is in the foreground and being actively used by users, so abide by the rules and don’t fight the platform. If you really need to do some long work in the background, use Service, which is the only way to signal to the operating system that your App is still in active use.

If the process terminates, Android provides some callbacks to save state, such as onSaveInstanceState() — yes, state again. Should we store the View information in the Bundle? Should we also save the state in Presenter into the Bundle? What about the state of the business logic? Again, a platitude, as discussed in the previous three sections.

All we need is a Model class that represents the entire state, and we can easily save the Model in the Bundle and restore it later. However, I personally think most of the time it’s better not to save the state and reload the entire screen, just like we did when we first started the App. Think of the NewsReader App that displays a list of news items. When the App was killed, we saved the state, and after 6 hours the user opened the App again and restored the state, our App might show outdated content. Therefore, in this case, reloading the data instead of storing the Model and state might be a better strategy.

5. Invariance of one-way data flow

I’m not going to discuss the advantages of immutabiliy here, because there are plenty of resources for that. We want an immutable Model. Why is that? Because we want a unique State source, we don’t want other components in the App to change our Model or State when passing the Model.

Let’s say we write a simple counter App with increment and decrement buttons and display the current counter value in a TextView. If our Model (in this case just the counter value, that is, an integer) is immutable, how do we change the counter?

I’m glad to be asked, because when the button is clicked, we’re not directly manipulating the TextView. My advice:

  • 1. WeViewThe layer should have a similarview.render(...)The method;
  • 2. OurModelIs immutable and therefore cannot be modified directlyModel;
  • 3.ViewRendering has one and only one source: business logic.

We sink the click event into the business logic layer. The business logic knows the current Model (for example, holds a private member Model that represents the current state), and then creates a new Model with incremental/decrement values based on the old Model.

So we have a one-way data flow where the business logic is used as a single source to create immutable Model instances, but that’s a bit much for a counter, isn’t it? Admittedly, yes, the counter is just a simple application. Most applications start as simple applications, but complexity increases rapidly — from my perspective, one-way data flow and immutable models are necessary to keep simple applications simple (for developers) as complexity increases.

6. Debugable and reproducible states

In addition, one-way data flow ensures that our application is easy to debug. The next time we get a crash report from Crashlytics, we can easily reproduce and fix the crash because all the necessary information has been attached to the crash report.

What is required information? That’s what the current Model and user want to do when a crash occurs (for example, click the decrement button). This is all the information we need to reconstruct the crash, which is easy to gather and attach to the crash report.

Without monomial data flow (for example, misuse of EventBus, or exposing the private field of CounterModels), or immutability (which leads to us not knowing who actually changed the Model), the bug is not so easy to reproduce.

7. Testable

The “traditional” MVP or MVVM improves the testability of applications. MVC is also testable: no one says we have to put all of our business logic into an Activity. Using the Model that represents the state, we can simplify the code for unit tests because we can simply check the assertEqual(expectedModel, Model). This allows us to avoid many objects that we have to Mock.

In addition, this reduces the number of tests needed to verify whether certain methods are called (such as mockito.verify (view, times(1)).showfoo ()). Ultimately, this makes our unit test code more readable, easier to understand, and easier to maintain. Because we don’t have to deal with a lot of implementation details of the actual code.

conclusion

In the first part of this blog series, we talked a lot about theory. Do we really need a blog devoted to the Model?

I think it’s really important to have an initial understanding of the Model to help us avoid some of the problems we might encounter. Model does not mean business logic, it is the business logic that generates the Model (i.e., an interaction, a use case, a repository or whatever you call in your APP).

In part 2, we’ll see the Model in action when we finally build a responsive App with Model-view-Intent. The demo APP is an application for a fictitious online store, so stay tuned.


Series directory

Building responsive Apps with MVI

  • Part 1: Model
  • Part 2: View and Intent
  • Part 3: State Reducer
  • Part 4: Independent UI Components
  • Part 5: Debugging with ease
  • Part 6: Restoring State
  • Part 7: Timing (SingleLiveEvent problem)
  • Part 8: In-App Navigation

Building responsive Apps with MVI

  • Building Responsive Apps with MVI (1): What is a Model
  • Build a responsive APP with MVI :View layer and Intent Layer
  • Building responsive Apps with MVI [C]: State folding
  • Build responsive Apps with MVI [4]: Independent UI components
  • Build responsive Apps with MVI: Easy Debug
  • Build a responsive APP with MVI [vi]: Restore state
  • Building responsive Apps with MVI [7]: Timing (SingleLiveEvent Problem)
  • Building responsive Apps with MVI [8]: Navigation

“Use MVI to build responsive APP” actual combat

  • Combat: Use MVI to build responsive & functional Github client

About me

If you think this article is valuable to you, welcome to ❤️, and also welcome to follow my blog or Github.

If you feel that the article is not enough, please also urge me to write a better article by paying attention to it — in case I make progress one day?

  • My Android learning system
  • About the article error correction
  • About paying for knowledge