📜 ⬆️ ⬇️

Reactive applications with the RxPM pattern. Goodbye MVP and MVVM

For a long time, I have been pondering over the RxPM pattern and even successfully applying it in production. I planned to first speak with this topic on Mobius , but the program committee refused, so I am publishing an article now to share my vision of the new pattern with the Android community.


Everyone is familiar with MVP and MVVM, but few know that MVVM is a logical development of the Presentation Model pattern. After all, the only difference between MVVM and PM is the automatic data binding ( databinding ).


This article focuses on the Presentation Model pattern with a reactive implementation of binding. Some people mistakenly call it RxMVVM, but it would be correct to call it RxPM, because it is a modification of the Presentation Model template.


This pattern is convenient to use in projects with Rx , as it allows you to make the application truly reactive. In addition, he does not have many of the problems of other patterns. The diagram below shows the various options and classifications for presentation templates:




Before turning to the description of the RxPM pattern, let's consider the most popular of them - MVP (Passive View) and MVVM. A detailed description of all the patterns and their differences, you can read in the previous article .


MVP vs PM vs MVVM


The general pattern of patterns can be represented as a diagram:



At first glance it may seem that there is no fundamental difference between them. But this is only at first glance. The differences are in the duties of the mediator and his method of communicating with the View. The model looks the same in all patterns. Its design is a complex and extensive topic, we will not dwell on it now. Let's start with the most popular pattern - MVP in the Passive View version. Consider his main problems.


MVP


In the classic MVP, the responsibility for maintaining and restoring the state of the UI lies with the View. Presenter only tracks changes in the model, updates the View through the interface and, conversely, accepts commands from View and modifies the Model.


However, when implementing complex interfaces, in addition to the data state in the model, there are additional UI states that are in no way associated with the data. For example, which element of the list is highlighted on screen or with what data the input form, information about the progress of the download process or requests to the network are filled. Restoration and preservation of the UI-state in the View presents major problems, since View is used to “die”. And information about network requests View in principle can not save. While View is disconnected from the presenter, the request is likely to end with some result.


Therefore, the work of restoring the state of the UI is carried out to the presenter. To do this, it is required to store in the presenter additional data and flags about the current state and to reproduce it every time the View is attached.


The second problem arises from the same condition that the View can be disconnected from the presenter at any time, for example, when the screen is rotated. Accordingly, the link to the View interface in the presenter will be reset. Therefore, you should always check for null when you need to update the View. This is rather tedious and cluttering up the code.


The third problem: it is necessary to describe the View interface in some detail, since it should be as stupid as possible. And the presenter has to call a lot of methods to bring the View to the desired state. This increases the amount of code.


PM


There is another pattern called Presentation Model, described by Martin Fowler. The essence of this pattern is that a special model is introduced, called the “view model”, which stores the state of the UI and contains the UI logic. PresentationModel should be considered as an abstract presentation that does not depend on any GUI framework. PresentationModel stores the state as a property ( property ), which then reads the View and displays on the screen. The main problem of the pattern is the synchronization of the PresentationModel and View states. You will have to take care of this yourself by applying the “Observer” pattern. Most likely, you will need to track changes to each property in order not to update the entire UI. It turns out quite a lot of boring and repetitive code.


MVVM


As you can see, MVVM is very similar to the Presentation Model. Not surprising, because it is its development. Only PresentationModel is called ViewModel, and the synchronization of the ViewModel and View state is performed using automatic data binding, i.e. databinding. But this pattern is not without flaws. For example, it is problematic to "cleanly" implement any animations or do something with the View from the code. You can read more about this in the article of my colleague Jeevuz .


Going a little ahead, here is his comment from our discussions of MVVM and RxPM
Starting to discuss and think about RxPM, I realized that this pattern combines what I liked in MVVM - the concept of a ViewModel'i as an interface over View, but at the same time does not contain the main drawback - duality. Which is logical, because there is no databinding. But at the same time, binding with Rx is not much more complicated than automatic binding with the Databinding Library, and it is very well suited for use in reactive applications.
As a consequence, RxPM solves the problem of states. Remember the rubik cube from my article? I described that a state can be described either by a set of fields or a set of actions ... So, RxPM combines these two ways in an interesting way: PresentationModel stores View states as a set of fields, but since these fields are represented by BehaviorSubjects (which emit the last event when subscribing), they are at the same time “actions”. And it turns out that any event that occurred in the background (not yet View) will arrive during the subscription. Fine!

But the most important and decisive disadvantage of all the above-mentioned patterns is that the interaction between the View and the mediator is carried out in the imperative style. Whereas our goal is to write reactive applications. The UI layer is a fairly large source of data flow, especially in dynamic interfaces, and it would be rash to use Rx only for asynchronous work with the model.


Reactive Presentation Model


We have already found out that the main problem of the Presentation Model pattern is state synchronization between PresentationModel and View. Obviously, it is necessary to use observable property - a property that can notify about its changes. In solving this problem, RxJava will help us, and at the same time we will get all the advantages of the reactive approach.


To begin with, we will look at the pattern scheme and then we will understand the implementation details:


So, the key element of RxPM is the reactive property . The first candidate for the role of Rx-property suggests BehaviorSubject . It stores the last value and gives it every time you subscribe.


In general, Subjects are unique in nature: on the one hand, they are extensions of the Observable, and on the other, they implement the Observer interface. That is, we can use the Subject as an outgoing data stream for the View, and in PresentationModel it will be the consumer of the incoming data stream.


However, Subject 's have flaws that are unacceptable to us. Under the Observable contract, they can terminate with the onComplete and onError events . Accordingly, if the Subject is subscribed to something that ends with an error, the entire chain will be stopped. View will stop receiving events and will have to subscribe again. In addition, by definition, Rx-property cannot send onComplete and onError events , as it is just a source of data (state) for the View. This is where Jake Wharton comes to the rescue with his RxRelay library. What would we do without him? Relay 'and lack the described disadvantages.


In the arsenal we have several subclasses:



But we cannot provide View access to Relay pits directly. Since it can accidentally put a value in a property or subscribe to a Relay , which is intended to receive commands from View. Therefore, it is required to present properties as Observable , and event listeners from View as Consumer . Yes, encapsulation will require more code, but on the other hand it will be immediately clear where the properties are, and where the commands are. Example of loading progress to PresentationModel (pm):


 //State private val progress = BehaviorRelay.create<Int>() //    property val progressState: Observable<Int> = progress.hide() //    ,      fun progress(): Observable<Int> = progress.hide() //Action private val downloadClicks = PublishRelay.create<Unit>() //    property val downloadClicksConsumer: Consumer<Unit> = downloadClicks //    ,      fun downloadClicks(): Consumer<Unit> = downloadClicks 

Now that we have defined the states and actions, we can only attach to them in View. For this we need another library of Jake Worton - RxBinding . When does he sleep at all?


 pm.progressState.subscribe { progressBar.progress() } //    downloadButton.clicks().subscribe { pm.downloadClicksConsumer } //    PM 

If there is no suitable Observable, then you can call consumer.accept() - directly from the widget's listener.


 pm.downloadClicksConsumer.accept(Unit) 

And now in practice


Now we collect all of the above in a bunch and analyze by example. PresentationModel design can be divided into the following steps:


  1. Determine which states of PresentationModel will be required for the View: data, loading status, errors to be displayed, etc.
  2. Determine what events can occur in View: clicks on buttons, filling in input fields, etc.
  3. When creating a PresentationModel, associate states, commands, and a model in a declarative style, as Rx allows.
  4. Bind View to PresentationModel.

Take for example the task of finding words in the text:



The search algorithm is hidden behind the facade of the interactor:


 data class SearchParams(val text: String, val query: String) interface Interactor { fun findWords(params: SearchParams): Single<List<String>> } class InteractorImpl : Interactor { override fun findWords(params: SearchParams): Single<List<String>> { return Single .just(params) .map { (text, query) -> text .split(" ", ",", ".", "?", "!", ignoreCase = true) .filter { it.contains(query, ignoreCase = true) } } .subscribeOn(Schedulers.computation()) } } 

In a specific example, it would be possible to do without Single and Rx at all, but we will keep the interfaces monotonous. Especially in real applications there could be a request to the network through Retrofit .


Next, we design PresentationModel.


States for View: list of found words, download status, search button activity flag. The enabled state for the button can be tied to the upload flag in PresentationModel, but for the View we have to provide a separate property. Why not just bind to the loading flag in View? Here we have to determine that we have two states: loading and enabled, but in this case it so coincided that PresentationModel binds them. Although in general they can be independent. For example, if it were necessary to block a button until the user enters the minimum number of characters.


Events from View: enter text, enter a search query and click on the button. Everything is simple: we filter the texts, combine the text and the search string into one object - SearchParams. By clicking on the button we do a search query.


Here is how it looks in code:


 class TextSearchPresentationModel { private val interactor: Interactor = InteractorImpl() // --- States --- private val foundWords = BehaviorRelay.create<List<String>>() val foundWordState: Observable<List<String>> = foundWords.hide() private val loading = BehaviorRelay.createDefault<Boolean>(false) val loadingState: Observable<Boolean> = loading.hide() val searchButtonEnabledState: Observable<Boolean> = loading.map { !it }.hide() // -------------- // --- UI-events --- private val searchQuery = PublishRelay.create<String>() val searchQueryConsumer: Consumer<String> = searchQuery private val inputTextChanges = PublishRelay.create<String>() val inputTextChangesConsumer: Consumer<String> = inputTextChanges private val searchButtonClicks = PublishRelay.create<Unit>() val searchButtonClicksConsumer: Consumer<Unit> = searchButtonClicks // --------------- private var disposable: Disposable? = null fun onCreate() { val filteredText = inputTextChanges.filter(String::isNotEmpty) val filteredQuery = searchQuery.filter(String::isNotEmpty) val combine = Observable.combineLatest(filteredText, filteredQuery, BiFunction(::SearchParams)) val requestByClick = searchButtonClicks.withLatestFrom(combine, BiFunction<Unit, SearchParams, SearchParams> { _, params: SearchParams -> params }) disposable = requestByClick .filter { !isLoading() } .doOnNext { showProgress() } .delay(3, TimeUnit.SECONDS) //      .flatMap { interactor.findWords(it).toObservable() } .observeOn(AndroidSchedulers.mainThread()) .doOnEach { hideProgress() } .subscribe(foundWords) } fun onDestroy() { disposable?.dispose() } private fun isLoading() = loading.value private fun showProgress() = loading.accept(true) private fun hideProgress() = loading.accept(false) } 

In the role of View, we will have a fragment:


 class TextSearchFragment : Fragment() { private val pm = TextSearchPresentationModel() private var composite = CompositeDisposable() private lateinit var inputText: EditText private lateinit var queryEditText: EditText private lateinit var searchButton: Button private lateinit var progressBar: ProgressBar private lateinit var resultText: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true //     pm.onCreate() } // ... onCreateView override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // ... init widgets onBindPresentationModel() } fun onBindPresentationModel() { // --- States --- pm.foundWordState .subscribe { if (it.isNotEmpty()) { resultText.text = it.joinToString(separator = "\n") } else { resultText.text = "Nothing found" } } .addTo(composite) pm.searchButtonEnabledState .subscribe(searchButton.enabled()) .addTo(composite) pm.loadingState .subscribe(progressBar.visibility()) .addTo(composite) // --------------- // --- Ui-events --- queryEditText .textChanges() .map { it.toString() } .subscribe(pm.searchQueryConsumer) .addTo(composite) inputText .textChanges() .map { it.toString() } .subscribe(pm.inputTextChangesConsumer) .addTo(composite) searchButton.clicks() .subscribe(pm.searchButtonClicksConsumer) .addTo(composite) //------------------ } fun onUnbindPresentationModel() { composite.clear() } override fun onDestroyView() { super.onDestroyView() onUnbindPresentationModel() } override fun onDestroy() { super.onDestroy() pm.onDestroy() } } //   RxKotlin /** * Add the disposable to a CompositeDisposable. * @param compositeDisposable CompositeDisposable to add this disposable to * @return this instance */ fun Disposable.addTo(compositeDisposable: CompositeDisposable): Disposable = apply { compositeDisposable.add(this) } 

You can see a complete example on GitHub .


Let's sum up


We met with the new RxPM pattern and took apart the minuses of other presentation templates. But I do not want to say unequivocally that MVP and MVVM are worse or better than RxPM. I also, like many, love MVP for its simplicity and straightforwardness. And MVVM is good in having automatic databing, although the code in the layout is an amateur.


But in modern applications with dynamic UI, a lot of event and asynchronous code. Therefore, my choice is leaning toward the reactive approach and RxPM. Let me give you the words from the presentation of Jake Whorton, why our applications should be reactive:


It is a single asynchronous source breaks imperative programming.
If you cannot simulate the entire system synchronously, then even one asynchronous source breaks imperative programming.

Of course, RxPM has both advantages and disadvantages.

Pros:



Minuses:



This is probably not a complete list. Write in the comments, what you see the pros and cons, it will be interesting to know your opinion.


So, if you feel confident with Rx and want to write reactive applications, if you are tired of MVP and MVVM with databinding , then you should try RxPM. Well, if you are already comfortable, I will not persuade you ;)


PS


The sophisticated Android developer most likely noticed that I didn’t say anything about the life cycle and about saving the PresentationModel during the turn. This problem is not specific to this pattern and deserves separate consideration. In my article, I wanted to focus on the very essence of the pattern: its pros and cons compared to MVP and MVVM. Also, such important topics as bilateral databinding , navigation between screens in the context of RxPM and some others were not touched upon. In the next article, we will try to talk with Jeevuz about how to start using RxPM in a real project and present some library solution that simplifies its use.


')

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


All Articles