Reactive applications with Model-View-Intent. Part 2: View and Intent
In the first part, we discussed what a model is, its relationship to the state and how a properly designed model helps to solve some problems in the development for Android. In this article, we will continue our path to creating reactive applications using the M odel- V iew- I ntent pattern.
Before we begin, we will briefly discuss the main idea of ​​MVI.
intent () : A function that accepts input from a user (for example, user interface events such as click events) and translates into what will be passed as a parameter to the model () function. This can be a simple string for setting the model value or a more complex data structure, such as an object.
model () : A function that uses the output from the intent () function as input to work with the model. The result of this function is a new model (with an altered state). Thus it is necessary that the data were immutable . In the first part, I gave an example with an application counter: we do not change an existing instance of the model, but create a new model according to the changes described in the intent. The model () function is only part of the code responsible for creating a new model object. In essence, the model () function calls the business logic of the application (be it Interactor, UseCase, Repository) and as a result returns a new model object.
view () : A function that receives as input model from model () and simply displays it. Normally the view () function looks like view.render (model) .
Let's return to our task. We want to create a reactive application. But is MVI reactive? What does reactivity really mean in this context?
By reactivity, we mean an application with a UI that responds to a state change. Since the state reflects the model, it is necessary for our business logic to react to events (intents), and at the output to create a model that could be displayed in View by calling the render (model) method.
Connect the dots with RxJava
We need the data stream to be unidirectional. This is where RxJava comes into play. When creating reactive applications with a unidirectional data flow, it is not necessary to use this particular library. However, RxJava is well suited for event programming. And since the UI is based on events, it makes sense to use it.
In this article I will describe the creation of a simple application for a fictional online store. In the application you can search for products and add them to the cart.
Let's start with the implementation of the search screen. First of all, I define a model that will be displayed using View, as described in the first part of this series of articles. I will write all classes of models with the ViewState suffix, since the model reflects the state.
Java is a strongly typed language, so I chose a type-safe approach to creating a model, dividing each substate within a class. The business logic will return an object of type SearchViewState , which may be an instance of SearchViewState.Error, etc. This is my personal preference, you can design the model in your own way.
Focus on business logic. Create a SearchInteractor that will be responsible for the search. The result of the execution will be a SearchViewState object.
SearchInteractor
publicclassSearchInteractor{ final SearchEngine searchEngine; public Observable<SearchViewState> search(String searchString){ if (searchString.isEmpty()) { return Observable.just(new SearchViewState.SearchNotStartedYet()); } return searchEngine.searchFor(searchString) .map(products -> { if (products.isEmpty()) { returnnew SearchViewState.EmptyResult(searchString); } else { returnnew SearchViewState.SearchResult(searchString, products); } }) .startWith(new SearchViewState.Loading()) .onErrorReturn(error -> new SearchViewState.Error(searchString, error)); } }
Let's look at the signature of the SearchInteractor.search () method: there is an input parameter searchString and an output parameter Observable <SearchViewState> . This suggests that on the observed stream, we expect an arbitrary number of SearchViewState instances. The startWith () method is needed to search SearchViewState.Loading before starting a search query. Then View will be able to show the progressBar during the execution of the search.
The onErrorReturn () method catches any exceptions that may occur during the execution of a search, and throws SearchViewState.Error . We cannot just use the onError () callback when subscribing to Observable. This is a common misconception in RxJava: the onError () callback should be used when all of the observed flow encounters fatal errors and the entire flow ends.
In our case, the error of not connecting to the Internet does not fall under the definition of fatal errors - this is just one of the states of our model. In addition, we will be able to switch to another state - SearchViewState.Loading - after the internet connection is available again.
Thus, we create an observable flow from business logic to View, which will emit a new model every time the state changes. We do not need the observed stream to terminate with an error connecting to the Internet, so such errors are treated as a state. Usually, in MVI, the observed stream never terminates (the onComplete or onError () methods are not called).
To summarize: SearchInteractor provides an observable stream of Observable <SearchViewState> and issues a new SearchViewState each time a state changes.
Consider what the View layer looks like, which should display the model. Earlier, I suggested that View had a function render (model) . In addition, the View should allow other layers to respond to UI events. In MVI, these events are called intents . In our case there is only one intent: the user searches for a product by entering a search query in the search field. In MVP there is a good practice to create an interface for the View layer, we will also do this for MVI.
In our case, View provides only one intent, but depending on the task there may be several of them.
In the first part, we discussed why using a single render () method is a good solution. Before creating a specific implementation of the View layer, let's take a look at how the final version will look like:
The render (SearchViewState) method should look succinct. In searchIntent () I use the RxBindings library. RxSearchView.queryText () creates an Observable that issues a string every time a user enters something into the EditText widget. I use filter () to start a search query after entering three or more characters. We do not need the search request to be sent to the server every time a user enters a new character, so I added a debounce () operator.
We know that the input data stream for this screen is the searchIntent () method, and the output data stream is the render () method.
The following video demonstrates how the interaction between these two streams occurs.
The question remains, how to link the intent and business logic? If you carefully watch the video, you will see the flatMap () operator in the middle. This indicates the presence of an additional component, which I did not mention - Presenter , which is responsible for connecting the layers.
What is MviBasePresenter , methods intent () and subscribeViewState () . This class is part of the Mosby library. It should say a few words about Mosby and how MviBasePresenter works. Let's start with the life cycle: MviBasePresenter doesn't have it. The bindIntent () method binds the intent from the View to business logic. As a rule, flatMap () or switchMap () is used to send an intent to business logic. This method is called once when View joins the Presenter, but is not called after the View joins the Presenter again, for example, after changing the orientation of the screen.
It may be asked whether MviBasePresenter can really survive the screen orientation change, and if so, how does Mosby ensure that the observed stream does not “leak”? For this purpose integer () and subscribeViewState () methods are intended.
intent () creates a PublishSubject inside the Presenter and uses it as a “gateway” for business logic. PublishSubject subscribes to the View. The invoice call (O1) actually returns a PublishSubject that is subscribed to O1.
After changing the screen orientation, Mosby disconnects the View from the Presenter, but only temporarily unsubscribes the internal PublishSubject from the View and reassigns the PublishSubject to the View intent when the View rejoins the Presenter.
subscribeViewState () does the same in the opposite direction. It creates inside the Presenter BehaviorSubject as a “gateway” from business logic to View. Since this is a BehaviorSubject, we can get an updated model from business logic even when View is disconnected from Presenter. The BehaviorSubject always stores the last value it received, and repeats it when View joins the Presenter again.
Simple rule: use the intent () method to wrap any intent. Use subscribeViewState () instead of Observable.subscribe (...).
What about other life cycle events, such as onPause () or onResume () ? I still think that the presenter does not need life cycle events . However, if you really think you need them, you can create them as an intent. In your View, pauseIntent () will appear, the launch of which initiates the Android life cycle, not the user's action.
Conclusion
In this part we talked about the basics of MVI and implemented a simple screen. This example is probably too simple to understand all the advantages of MVI. There is nothing wrong with MVP or MVVM, and I’m not saying that MVI is better than other architectural templates. Nevertheless, I believe that MVI helps us write more elegant code for complex problems, as we will see in the next section, which will talk about state reducer .