📜 ⬆️ ⬇️

Reactive applications with Model-View-Intent. Part 1: Model

When working with the Android platform, I ran into a lot of problems because I designed my models incorrectly. My applications were not reactive enough. Now using RxJava and Model-View-Intent (MVI), I finally achieved the desired level of reactivity. About this I am writing a series of articles. In the first part I will talk about the model and explain how it is important.

What did I mean when I said that I designed the models incorrectly? Many architectural patterns separate the “View” from the “Model”. The most popular in the development of Android - Model-View-Controller (MVC), Model-View-Presenter (MVP) and Model-View-ViewModel (MVVM). By name it is clear that they all use the “Model”. I realized that most of the time I didn’t have a model at all.

Example. The task is to download a list of people from the server. The “traditional” implementation of MVP looks like this:
')
class PersonsPresenter extends Presenter<PersonsView> { public void load(){ getView().showLoading(true); //  ProgressBar   backend.loadPersons(new Callback(){ public void onSuccess(List<Person> persons){ getView().showPersons(persons); //      } public void onError(Throwable error){ getView().showError(error); //       } }); } } 

What is a model and where is it? A model is not a backend or List that we receive. This is the entity that the View displays along with other load indicators or error messages. From my point of view, the model should look like this:

 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; } } 

And then Presenter can be implemented as follows:

 class PersonsPresenter extends Presenter<PersonsView> { public void load(){ getView().render( new PersonsModel(true, null, null) ); //  ProgressBar backend.loadPersons(new Callback(){ public void onSuccess(List<Person> persons){ getView().render( new PersonsModel(false, persons, null) ); //    } public void onError(Throwable error){ getView().render( new PersonsModel(false, null, error) ); //     } }); } } 

Now View has a model that is displayed on the screen.

The first definition of MVC was proposed by Trygve Reenskaug in 1979. It reflects a similar idea: View observes how the model changes. The researchers described the term MVC a large number of templates that do not fall under the formulation of Reenskaug. For example, server developers use MVC frameworks, iOS has a ViewController. There are questions:


Today, the term MVC is used and interpreted incorrectly, so we will suspend the discussion so that it does not get out of control. Let's return to my initial statement. In the development of Android, we face a number of problems that can be avoided using the model:

  1. Condition problem
  2. Screen orientation changes.
  3. Backsight navigation.
  4. Death process.
  5. Immutability and unidirectional data flow.
  6. Debugging and playback of states.
  7. Testability

Let's see how the “traditional” implementation of MVP and MVVM copes with problems, and how the model eliminates common errors.

Condition problem

Reactive applications are a trendy definition of UI applications that respond to state changes. The condition is what we see on the screen. For example, the loading state when View displays the ProgressBar. As frontend developers, we tend to focus on the UI: a good UI determines how successful the application is, how many people will use it.

Take another look at the Presenter code above - not the one that uses PersonsModel. The presenter determines the state of the UI, it is he who tells View what to display. The same applies to MVVM. In this article, I highlight two implementations of MVVM: one uses Android Data Binding, the other RxJava. In MVVM with Data Binding the state is in ViewModel:

 class PersonsViewModel { ObservableBoolean loading; // ...       public void load(){ loading.set(true); backend.loadPersons(new Callback(){ public void onSuccess(List<Person> persons){ loading.set(false); // ...  ,    } public void onError(Throwable error){ loading.set(false); // ...  ,      } }); } } 

In MVVM with RxJava, we do not use the Data Binding mechanism, but associate Observable with UI widgets in View:

 class RxPersonsViewModel { private PublishSubject<Boolean> loading; private PublishSubject<List<Person> persons; private PublishSubject loadPersonsCommand; public RxPersonsViewModel(){ loadPersonsCommand.flatMap(ignored -> backend.loadPersons()) .doOnSubscribe(ignored -> loading.onNext(true)) .doOnTerminate(ignored -> loading.onNext(false)) .subscribe(persons) //      } public Observable<Boolean> loading(){ return loading; } public Observable<List<Person>> persons(){ return persons; } //       (   onNext())  Persons public PublishSubject loadPersonsCommand(){ return loadPersonsCommand; } } 

Code examples are not perfect, your implementation may look different. The main thing is that in MVP and MVVM state is controlled by a Presenter or ViewModel. What this leads to:

1. Business logic has its own state, just like Presenter or ViewModel. You are trying to synchronize the state of business logic and Presenter so that they are the same. Set the visibility of a widget directly to View, or Android itself restores the state from the bundle during the re-creation.

2. Presenter and ViewModel have an arbitrary number of input points. View triggers the event that Presenter handles. But Presenter also has many output channels - like view.showLoading () or view.showError () in MVP. And ViewModel offers multiple Observables. This leads to conflicting states of View, Presenter, and business logic, especially when working with multiple threads.

At best, visual errors will appear, for example, the display of the loading indicator (loading status) and the error indicator (error status) at the same time.


In the worst case, in the crash reporting tool — for example, in Crashlytics — you will get a bug report that you cannot reproduce and fix quickly.

Consider a situation where we have a single source of state, transmitted from the bottom up - from business logic to View. We already saw a similar idea at the beginning of the 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; } } 

The model reflects the state - if you understand this, you will avoid many state-related problems. Presenter will have only one output source: getView (). Render (PersonsModel). This reflects the simple mathematical function f (x) = y. There may be several input values, for example f (a, b, c), but the output will always be one.

Not everyone likes mathematics, but mathematicians, unlike programmers, do not know what a bug is. Understanding the essence of the model and how to properly form it is very important: it solves the problem of state.

Screen orientation changes

In Android, changing the screen orientation is a difficult problem. Of course, you can ignore it and reboot everything at every turn of the screen. Most of the time your application is offline, and data comes from a local database or another local cache. As a result, after changing the screen orientation, data loads very quickly.

However, I personally do not like to see the download indicator even for a few milliseconds, because, in my opinion, this does not look like a smooth interface .. Therefore, developers use MVP with retain presenter. While the screen is rotated, the View can be separated (and deleted), while the Presenter continues to exist in memory and the View is attached again to it.

The same idea is possible when using MVVM with RxJava. But as soon as View unsubscribe from its ViewModel, observable stream - the observed stream - is destroyed. This can be avoided using Subjects. In MVVM with Data Binding, a ViewModel is directly connected to the View by the Data Binding mechanism. To avoid memory leaks, you need to destroy the ViewModel when the screen orientation changes.

The main problem with the retain presenter or ViewModel is how to return the View state to what it was before turning the screen so that View and Presenter were in the same state again? I wrote the Mosby MVP library, it includes the ViewState component, which synchronizes the state of your business logic and View. Moxy, another MVP library, uses commands and reproduces the View state after the screen orientation has changed:
image
There are other solutions to the View state problem. Let's take a step back and summarize: all these libraries are trying to solve the state problem.

So again: if we have one “Model”, reflecting the current “State”, and one method to handle the “Model”, we solve the state problem simply by calling getView (). Render (PersonsModel) (with the latest Model, when we re-attach View to Presenter).

Backsight navigation

Does Presenter or ViewModel save if we no longer use View? For example, Fragment (View) has been replaced by another Fragment, and the user has moved to another screen. View is no longer attached to Presenter - it will not be able to update View with the latest data from the business logic. What if the user comes back, for example, by pressing the “Back” button and deleting the last transaction in the back stack? Do I need to reload data or use an existing Presenter?

The user, returning to the previous screen, wants to continue working with the application in the same place where he stopped. We discussed this problem when we talk about changing the orientation of the screen. The solution is obvious: as soon as the user returns from the back stack, call getView (). Render (PersonsModel) with a model that reflects the state.

Death process

There is a common misunderstanding in Android development that the death of the process is bad, and we need libraries that help restore the state after the death of the process. The process dies in two cases: if the Android operating system needs resources for other applications or to save battery. This will not happen when your application is in the foreground and is being actively used. Do not argue with the platform. If you have long operations in the background, use the Service - so the system will understand that the application is used.

If the process is dead, Android will provide some life-cycle methods, such as onSaveInstanceState (), to save the state. And again we ask questions:


We conclude. As described in the previous paragraphs, we need a model that reflects the complete state. Then we just save the model in a bundle and restore it later. But often the best solution is not to save the state, but to reload the entire screen, like when you first started the application. Submit an application that displays a list of news. Our application is killed, we save the state. After 6 hours, when the user opens the application and the status is restored, the application displays the outdated content. In this scenario, it is better to reload the data rather than save the state.

Immutability and unidirectional data flow

Let's not talk about the benefits of immutability, we have a lot of information about it. We want to get a single source of reliable data. This requires an immutable model that reflects the state. Other components of the application should not affect our model or state.

Imagine that we are writing a simple counter application that displays the current value in TextView and has only two buttons - “Increase” and “Reduce”. In this case, our model is the counter value. If this is an immutable model, how do we change the counter? We will not change the TextView immediately after each button click.

  1. Our model should use only the view.render (...) method.
  2. Direct model changes are not possible.
  3. We must have the only source of reliable data - business logic.

In business logic, there is a private field with the current model, and it creates a new model with a larger or smaller value relative to the old model.

image

We establish a unidirectional data flow, where business logic is a single data source that creates immutable model instances. It looks technically complicated for a simple counter, because the counter is a simple application. Many applications developers start with simple things, but then complicate. Unidirectional data flow and immutable model are important even for simple applications — for developers, they will remain simple, even when complexity increases.

Debug and reproducible states

Unidirectional data flow provides simple debugging in our application. It would be nice to get a full crash report from Crashlytics to reproduce and quickly fix this crash. All the information we need is the current model and the action that the user wanted to take at the time of the crash. For example, pressing the "Reduce" button in the counter. This is enough to reproduce the failure; this information is simply logged and attached to the failure report. Without a unidirectional data flow, it was difficult to find out, for example, that someone misused the EventBus and launched our CounterModel in an incomprehensible way. Without immutability, we could not figure out who and where exactly changed our Model.

Testability

The “traditional” use of MVP or MVVM improves application testability. MVC can also be tested: it is not necessary to place all business logic in the Activity. With a model that reflects state, we can simplify our unit tests by simply writing assertEquals (expectedModel, model).
This will save us from creating a lot of "Mock" objects.

We will get rid of tests confirming that a particular method has been called, i.e.
Mockito.verify (view, times (1)). ShowFoo ()). It will be easier for us to read, understand and maintain the unit test code - we don’t have to work with the details of the implementation of this code.

Conclusion

In the first part of a series of articles on MVI, we talked more about theoretical things. Why do we need a separate article about the model?

The model helps to avoid a number of problems:

  1. Condition problems
  2. Screen orientation changes.
  3. Backstay navigation.
  4. Death process.
  5. Immutability and unidirectional data flow.
  6. Debugging and state playback.
  7. Testability

In their projects, developers have differently called business logic - Interactor, Usecase, Repository. A model is not a business logic. Business logic produces a model.

In the next article we will look at all this theoretical part about the Model in work. For example, create a reactive application where we use Model-View-Intent. We are going to create a demo application for a fictional online store.

Source: https://habr.com/ru/post/325376/


All Articles