📜 ⬆️ ⬇️

Moxy - MVP implementation for Android with a pinch of magic

What is MVP


MVP is a way of sharing responsibility in the application code. Model provides data for Presenter . View performs two functions: it responds to commands from the user (or from UI elements), sending these events to the Presenter and modifies the gui on demand of the Presenter . Presenter acts as a link between View and Model . Presenter receives events from View , processes them (using or not using the Model ), and commands the View about how it should change itself.

This approach to the division of responsibility has a number of advantages:
  1. Writing tests to the code is greatly simplified.
  2. Easy to change a part without breaking another
  3. The code is broken into small pieces, due to which it becomes more understandable and readable

At the same time, of course, there are also disadvantages:
  1. Code gets bigger
  2. You need to get used to this approach.
  3. At the moment, not very common ( but well-known ) approach, so you have to tell everyone about it


MVP in Android


Activity in Android is God object . It usually has the following responsibility:

The saddest thing is that our God Object is not immortal - Activity also dies when the configuration changes.

MVP removes some of the responsibility for the Activity. All work with asynchronous tasks goes to Presenter . All business logic is in Presenter and Model . Activity, in turn, becomes View . She starts to simply display what Presenter says and sends events to Presenter so that he can decide what to do next.
')
Before writing our solution, we studied many articles and implementations of the MVP concept in Android (see links at the end of the article). Based on the analysis, a list of solution requirements has been established:
  1. View should bind to existing Presenter when changing configuration
  2. After binding the View to an existing Presenter , the View should display the current state of the Presenter.
  3. Presenter should be able to ( if necessary ) live no matter who subscribes to it or unsubscribes from it.

At the moment, none of the existing solutions can do all these points at the same time. As it seemed to us at first, the Mosby library was the most suitable for us. But later it turned out that when using it, we would have to write too much code every time. Especially, for the implementation of the first two points from our list of requirements. Therefore, it was decided to develop their own decision.

Moxy - theory


Our solution is very different from all the others (even the MVP concept itself was modernized) in that ViewState was mixed up between View and Presenter . And it is absolutely necessary there. He is responsible for ensuring that each View always looks exactly as the Presenter wants. ViewState stores a list of commands that were transferred from Presenter to View . And when the “new” View joins the Presenter , the ViewState automatically applies to it all the commands that the Presenter issued earlier. Thus, it turns out that no matter what happens with View due to the fault of Android, View will remain in the correct state anyway. To do this, you will only need to get used to changing the View solely by the commands from the Presenter . Note that this is one of the basic rules of MVP and applies not only to Moxy.

A schematic illustration of how this works:

What happens on this scheme:
  1. In the View event occurs that is passed to the presenter
  2. Presenter passes command in viewstate
  3. Presenter starts an asynchronous request in Model
  4. ViewState adds command in the queue of commands, and then passes it to the View
  5. View brings itself to the state specified in the command.
  6. Presenter gets query result from model
  7. Presenter passes two commands to ViewState and
  8. ViewState saves commands and to the command queue and send them to the View
  9. View brings itself to the state specified in the commands. and
  10. New / recreated View joins an existing Presenter
  11. ViewState transfers the saved command list to a new / recreated View.
  12. New / recreated View brings itself to the state specified in the commands , and


Moxy - features


Moxy has several significant advantages over other solutions:
To do this, Moxy has several mechanisms that can be combined with each other as you please. The most powerful mechanisms are annotations, on the basis of which the code is generated. And during the execution of the program, the tool called MvpDelegate begins to fully use the generated code.

The following annotations are available:

All this further.

Moxy - MvpPresenter


Each application contains some business logic. In the MVP concept, all business logic is located in the Presenter and in the Model . In fact, this means that you practically do not program in View . In order for your Presenter not to become a God Object, you need to separate each separate block of business logic into a separate Presenter . In this case, you will get a lot of Presenter , but they will be very simple and understandable. For example, if you had two business logic on one screen, and then they split into 2 different screens, then you simply change the View. A Presenter what they were, and will remain so. Also, in this case, you can easily reuse one Presenter in several places (for example, BasketPresenter, pass-through through the entire application). It also simplifies code testing - you just check a small Presenter that it does everything right.

For Presenter in Moxy the class MvpPresenter<View extends MvpView> . MvpPresenter contains an instance of ViewState , which at the same time must implement the same type of View that came in MvpPresenter . This ViewState instance can be accessed from the public View getViewState() method. And during development you do not think that you are working with ViewState , but simply give commands for View through this method how to change it. There are also methods for attaching / decoupling View from Presenter ( public void attachView(View view) and public void detachView(View view) ). Note that multiple views can be associated with one Presenter . They will always be up to date (at the expense of ViewState ). And if you want the View binding / decoupling to pass not through the standard ViewState field, you can redefine these methods and work with the incoming View as you wish. For example, you might want to use a non-standard ViewState , which does not implement the View interface, if you need to.

In the class MvpPresenter is also an interesting method protected void onFirstViewAttach() . It is very important to understand when this method will be called and why it is needed. This method is called when any View is bound to a specific Presenter instance for the first time. And when another View is attached to this Presenter , the state from ViewState will be applied to it. And here it does not matter, this new View is a completely different View , or recreated as a result of a configuration change. This method is suitable, for example, to load a news list when you first open the news list screen.

At the moment when the command came to View , you may need to understand, is this a new command, or is it a command to restore the state? For example, if this is a fresh command, then you need to apply a command with animation. Otherwise, do not apply animation. This can be done through different StateStrategy, or through complex flags in the Bundle savedState . But the correct solution would be to use the Presenter (or ViewState ) method. public boolean isInRestoreState(View view) , which tells you what state the particular View is in . This way you can understand whether you need animation or not.

Moxy - MvpView and MvpViewState


The simplest component of MVP is View . You need to create an interface that is inherited from the MvpView interface marker and describe in it methods that the MvpView will be able to perform. In addition to View , our library has a ViewState entity, which is directly related to View . ViewState is a descendant of MvpViewState<View extends MvpView> . It manages one, or several, View (all of one type View ). And every time a team from the Presenter comes to ViewState , ViewState sends it to all the View he knows about. Also, MvpViewState has a protected abstract void restoreState(View view) method, protected abstract void restoreState(View view) , which will be called when any View is recreated, or when a new View is bound to the Presenter to the ViewState . After this method is completed, the “new” View will take the desired state.

It is worth noting that MvpViewState stores a list of all View attached to it. And it will be good if you do not forget to untie the View that has already been destroyed. But if you suddenly forget to do this, do not worry much - MvpViewState does not store direct links to the View , but the WeakReference , which will still help the GC. And if you use a mechanism such as MvpDelegate , then you can not worry about it - it binds the View to the Presenter and untie them.

Moxy - @GenerateViewState and @InjectViewState


Since ViewState in most cases is a rather monotonous layer between View and Presenter , a code generator was written that does all the dirty work for you. By applying the @GenerateViewState annotation to your View interface, you will get the generated ViewState class. And so that you don’t have to independently search and create an instance of this class in Presenter , there is the @InjectViewState annotation. Simply apply it to your Presenter class. Further MvpPresenter will do everything itself - it will create an instance of this ViewState , fold it to itself as a field and use it everywhere. You just need to work with the public View getViewState() MvpPresenter from MvpPresenter .

In case you do not want to use @GenerateViewState , but your ViewState implements the View interface, you can still use the @InjectViewState annotation. In this case, pass to this annotation, as a parameter, the class of your ViewState .

Be careful when applying the @InjectViewState annotation to a typed Presenter.
For example, if you have this code:
 @InjectViewState public class MyPresenter<T extends MvpView> extends MvpPresenter<T> { // pass } 
The Annotation processor will not correctly understand the View class, whose ViewState should be used. In this case, you can explicitly pass the View class to the view parameter of the @InjectViewState annotation.

Also note that an interface annotated with @GenerateViewState must be non-typed.
The catch lies in the fact that when generating code, we need to know the types of all parameters of all methods. Otherwise, the resulting code will not work.

Therefore, you can write such code:
 @GenerateViewState public interface ConcreteInterface extends AbstractInterface<String> { // pass } 

You cannot write such code:
 @GenerateViewState public interface ConcreteInterface<Type> extends AbstractInterface<Type> { // pass } 

Moxy - StateStrategy for commands in ViewState


By default, all commands for the View are saved in ViewState simply in the order in which they arrived there. And after the teams have been applied, they continue to lie in this queue. But this behavior can be changed by applying the @StateStrategyType annotation to the View interface and to its methods. At the input, this annotation receives a parameter in which you must specify the StateStrategy class that you want to use. If you apply this annotation to the entire View interface, then those methods for which a strategy is not specified will use this strategy.

StateStrategy manages the command queue through two methods: void beforeApply and void afterApply . The first method will be called before the command is sent to the View (the beforeApply method will be called as soon as a command from the Presenter arrives). At this point, in the default strategy, the command is added to the queue. The second afterApply method will be called each time the command is applied to the View . Both in the first and in the second method you can change the list of commands as you like.

Let's look at strategies that are already implemented in Moxy:

If you have some specific logic and you lack these strategies, then you can make your own strategy. In this case, the method of tagging methods will help you. You can pass the tag parameter to the @StateStrategyType annotation (the default is the name of the method). Then, using this tag, you can see in the void beforeApply(List<ViewCommand<View>> currentState, ViewCommand<View> incomingCommand) and void afterApply(List<ViewCommand<View>> currentState, ViewCommand<View> incomingCommand) that for ViewComand came to you (from the ViewCommand String getTag() method).

Before writing your strategies, look at the code already implemented - maybe it will be useful to you.

Moxy - MvpDelegate and the life cycle of MvpPresenter


By itself, the Presenter is not created anywhere, is not stored anywhere, and is not available anywhere. And so that you do not have to invent anything to solve these problems, we made a mechanism such as MvpDelegate . He ensures that, where his copy is, all Presenters are correctly initialized. For this, all you have to do is to transfer to it all the main points of the life cycle of your View . You can see which methods to invoke when in the MvpActivity or MvpFragment .

In order for MvpDelegate find all the Presenters , you must annotate them with @InjectPresenter. This annotation is very powerful. Through it, you can control how long the Presenter will live. If you want the Presenter to live for as long as there is a View in which it is contained (+ while the configuration is changing), simply add this annotation to the Presenter field. In case you want Presenter to live no matter who subscribes to it, you will need to do two things. The first is to inform MvpDelegate that the Presenter is not tied to the life cycle of the one who requested it. For this, you need to set the value of the type parameter of the annotation @InjectPresenter as PresenterType.GLOBAL. The second is that you must pass information to MvpDelegate on which he can find the Presenter you need in the repository of all Presenter . There are two options for how to do this:

First option. In the @InjectPresenter annotation you set the value for the tag parameter. Then MvpDelegate will try to find the global Presenter with the tag. If he finds it, then simply install it in this field. Otherwise, it will create a suitable Presenter , add it to the repository, and install it in this field. Taking into account the fact that several Views can be linked to one Presenter , this mechanism opens up many possibilities in front of you.

The second option (for the parzimerizirovannogo tag). In fact, it is similar to the first option. The only difference is that in the second case you cannot know in advance which Presenter tag will be. Those. tag must be generated dynamically. Then you have to try a little:
  1. Create your own PresenterFactory implementation
  2. In the @InjectPresenter annotation, set the parameters:
    • In factory, set your PresenterFactory class
    • In presenterId, set the Presenter string identifier (this is necessary to distinguish Presenter classes with the same factories in the same class)
  3. Get your own interface containing one and only one method that will return a parameter for the desired type of factory:
    • Annotate this interface as @ParamsProvider (PresenterFactoryClass), passing annotations, as a parameter, to the class of your PresenterFactory
    • Describe the method that will return the parameter, should receive one String parameter as input (this parameter will contain the same presenterId parameter from the @InjectPresenter annotation)
  4. The object that contains a Presenter , in the annotation @InjectPresenter of which this PresenterFactory is indicated, must implement the one created in par. 3. interface

Here you should know that you did not think that this place is too confusing. So it is, it is confused. Just know that if you need this functionality, follow this short list of rules, and you will understand everything and you will succeed.

In addition to the above functionality, MvpDelegate can be a parent / child delegate for another. This is necessary so that you can automate the Presenter life cycle not only within the Activity / Fragment, but also within other elements that do not have an independent life cycle (for example, in the adapter or even in the ViewHolder of the adapter element). If you set one MvpDelegate as the parent of another MvpDelegate , then the child delegate will receive all the events of the life cycle of the parent delegate. To do this, simply call the public void setParentDelegate(MvpDelegate delegate, String childId) method on the target MvpDelegate . As a delegate he expects to receive the parent MvpDelegate . As childId , you must specify a unique identifier by which the local Presenter of one delegate-child will differ from the local Presenter of another delegate-child.

Note that if the MvpDelegate has already been called on the onCreate . then you need to call the onCreate method on the onCreate delegate onCreate . Why is it important? To understand this, let's see how MvpDelegate works.

MvpDelegate in addition to controlling the initialization of the Presenter fields, does another very important thing. He binds and untips View from Presenter . Binding the View to the Presenter occurs in the onStart method, and onDestroy in the onDestroy method. Fragment little different, see github .
Why in these methods?
After calling onCreate MvpDelegate , all fields marked with the @InjectPresenter annotation are ready to work. But View is not yet attached to them. The view will be bound to the Presenter after the MvpDelegate method MvpDelegate void onStart() .

After that, the Presenter can interact with the View (and then, if the Presenter was first attached to this View , the Presenter void onFirstViewAttached() method will be called). After calling void onDestroy() on MvpDelegate , the View will be decoupled from the Presenter . And then there are two questions. First, why is View not bound to Presenter in onCreate , but in onStart ? Secondly, once the binding has happened in onStart , then why is the onStop not in onStop , but in onDestroy ? Quite reasonable questions. And the answer to them is that it is a) more convenient, and b) easier. More conveniently, ViewState is applied to the View as soon as the View has been bound to the Presenter . And if you bind the View to the Presenter in onCreate , it turns out that you will need to call the onCreate delegate method yourself in the Activity after you complete all the Android View initialization onCreate Activity. It is not comfortable. It is convenient to simply make one Activity from which all the activities of your application will inherit, and in the onCreate method of this Activity simply execute the onCreate delegate onCreate . And taking into account that the view is bound in onStart , there will be no problems. Secondly, if you unbind the View in onStop , then the binding will definitely happen every time onStart (now the binding of the View occurs in onStart , only if onCreate was done onCreate ). This means that ViewState will be restored with every onStart . This means that the entire state will be rolled back anew, even if the Activity View was not destroyed, but simply became invisible for a while. Therefore, decoupling the View from the Presenter occurs in onDestroy . Note: onDestroy will not be called if Android decides to kill the Activity process, but in this case the Presenter will be destroyed.

MvpDelegate uses special storage for the Presenter . Access to this repository is obtained through MvpFacade . MvpFacade - contains the Presenter storage and some other elements designed to help MvpDelegate to do its job optimally. Although MvpFacade is a singleton, it would be great if you run its public static void init() method, for example, in the Application's onCreate() method. Or you can inherit your Application from MvpApplication supplied in Moxy. Then at the moment when MvpDelegate turns to this singleton, it will be ready for work.

Moxy - Model


An important element of MVP is Model . But in Moxy, this part of the MVP is in no way affected. The thing is that this makes no sense. Each project has its own requirements for Model . In some places, Model is just a set of classes for working with an API and the work itself with an API (for example, via Retrofit). Somewhere in the Model also includes additional business logic. In some projects, the use of the Clean Architecture approach is relevant. In this case, additional entities appear inside the Model , for example, Interactor and Repository . And given the fact that Presenter is completely untied from the Activity life cycle, you can safely create an instance of a specific Model inside the Presenter and work with it. Using DI you can connect the desired Model in Presenter . And in the future, using the same DI, quietly replace Model for tests.

In any case, it is extremely convenient to use Rx to work with the Model . Then you can make Model's public methods return Observable . In this case, it will be easy to make the interaction of Model ⇒ Presenter , and at the same time Model ⇔ Model . This will make it possible to easily make parallel execution of queries from Presenter to Model .

Moxy - total


As a result, we have a library that solves all the problems of the life cycle. You will always show the user exactly the state that is relevant to him, and at the same time, you will not have to do anything extra. Just describe all the commands for View by separate methods. And avoid changing the View from the View itself. If you showed a dialogue with a team from Presenter , then when closing the dialogue, there should be a command from Presenter . Otherwise, ViewState will show you the dialog again after changing the configuration.

I would like to note that the library does not limit you in any way in choosing the implementation of multithreading in your application. You can use Rx, AsyncTask, Thread, Executor. The main thing is to be careful, work with View only from the main stream. Also, Moxy will not solve problems with commit() fragments after running onSaveInstanceState() . Therefore, remember to close the transaction using commitAllowingStateLoss() . Also, it will not solve problems with memory leaks - if you pass the link to Context / Activity / Fragment to Presenter (and then to ViewState ), then the memory may leak. Be careful.

Useful materials


Moxy would not have turned out the way it turned out if it were not for the numerous works of other people. Here are some of them:

Moxy –


To connect Moxy to your project, just add it in dependencies. Moxy consists of three parts. One of them is responsible for providing you with a Moxy SDK. It's pretty easy to connect:
 dependencies{ ... compile 'com.arello-mobile:moxy:1.1.1' } 

If you want to have access to such auxiliary classes as MvpApplication, MvpActivityand MvpFragment, also connect moxy-android:
 dependencies{ ... compile 'com.arello-mobile:moxy-android:1.1.1' } 

The other part is responsible for handling annotations and generating code. And here you need to decide.

If you do not have any special requirements, your project is a normal Android project, and you do not want the generated code to be available from your when, then connect the dependency like this:
 dependencies{ ... provided 'com.arello-mobile:moxy-compiler:1.1.1' } 

If you want to have direct access to the generated code, then you should use android-apt :
  1. Modify your project's build.gradle:
     buildscript { dependencies { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' } } 

  2. Modify your application's build.gradle:
     apply plugin: 'com.neenbedankt.android-apt' dependencies { ... apt 'com.arello-mobile:moxy-compiler:1.1.1' } 

Library sources can be found on Github: https://github.com/Arello-Mobile/Moxy

A full example application using Moxy: https://github.com/Arello-Mobile/Moxy/tree/master/sample-github

At the moment when we compile a representative list of questions on our library, on how to use it, on MVP as a whole, a separate article will be made, which will highlight the most popular / interesting questions. Questions can be asked here in the comments, write to me (@senneco) and another author of the library - Xanderblinov . Or you can contact the entire Android development department at Arello Mobile by writing to java-developers@arello-mobile.com .

From the authors of the Moxy senneco library
and Xanderblinov

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


All Articles