In part 1, we figured out why we need strategies in Moxy and when it is appropriate to apply each of them. In this article, we will look at the mechanism of how strategies work from the inside, understand when we may need custom strategies, and try to create our own.
Why does Moxy support the creation of
custom strategies? When designing the library, we (I) tried to take into account all possible cases, and the built-in strategies cover almost one hundred percent of them. However, in some cases, you may need more power over the situation, and we did not want to limit you. Consider one of these cases.
Presenter is responsible for choosing a business lunch, which consists of a burger and a drink.
Commands, depending on the function, are divided into the following types:
So, we want to be able to separately manage the queues for burger and beverage teams. There will not be enough default strategies for this, in addition we need burger and beverage! Let's invent them, but first, let's look at how the mechanism for the use of commands works and what strategies influence.
Let's start from afar: the ViewState is created in the presenter constructor, all commands are proxied through it. ViewState contains the command queue — ViewCommands (the class that is responsible for the list of commands and strategies) and the View list. The View list can contain as many View, if you are using a presenter like Global or Weak , or none (in the situation when your piece went into the back stack).
First, let's look at what the strategy is:
public interface StateStrategy { <View extends MvpView> void beforeApply( List<ViewCommand<View>> currentState, ViewCommand<View> incomingCommand); <View extends MvpView> void afterApply( List<ViewCommand<View>> currentState, ViewCommand<View> incomingCommand); }
This is an interface with two methods: beforeApply and afterApply . Each input method accepts a new command and a current list of commands, which will change (or remain unchanged) in the method body. For each team, we can get a tag (this is a string that can be specified in the annotation StateStrategyType ) and the type of strategy (see listing below). How exactly to change the list we decide, relying only on this information.
public abstract class ViewCommand<View extends MvpView> { private final String mTag; private final Class<? extends StateStrategy> mStateStrategyType; protected ViewCommand( String tag, Class<? extends StateStrategy> stateStrategyType) { mTag = tag; mStateStrategyType = stateStrategyType; } public abstract void apply(View view); public String getTag() { return mTag; } public Class<? extends StateStrategy> getStrategyType() { return mStateStrategyType; } }
Let's understand when we will call these methods. So, we have a SimpleBurgerView interface that can only add a little cheese (II).
interface SimpleBurgerView : BaseView { @StateStrategyType(value = AddToEndSingleStrategy::class, tag = "Cheese") fun toggleCheese(enable: Boolean) }
Consider what happens when you call the toggleCheese method on the generated class LaunchView $$ State (see listing):
@Override public void toggleCheese( boolean p0_32355860) { ToggleCheeseCommand toggleCheeseCommand = new ToggleCheeseCommand(p0_32355860); mViewCommands.beforeApply(toggleCheeseCommand); if (mViews == null || mViews.isEmpty()) { return; } for(com.redmadrobot.app.presentation.launch.LaunchView view : mViews) { view.toggleCheese(p0_32355860); } mViewCommands.afterApply(toggleCheeseCommand); }
1) ToggleCheeseCommand is created (see listing below)
public class ToggleCheeseCommand extends ViewCommand<com.redmadrobot.app.presentation.launch.SomeView> { public final boolean enable; ToggleCheeseCommand( boolean enable) { super("toggleCheese", com.arellomobile.mvp.viewstate.strategy.AddToEndStrategy.class); this.enable = enable; } @Override public void apply(com.redmadrobot.app.presentation.launch.SomeView mvpView) { mvpView.toggleCheese(enable); } }
2) the beforeApply method is called for the ViewCommands class for this command. In it, we get the strategy and call its beforeApply method. (see listing below)
public void beforeApply(ViewCommand<View> viewCommand) { StateStrategy stateStrategy = getStateStrategy(viewCommand); stateStrategy.beforeApply(mState, viewCommand); }
Hooray! Now we know when the strategy's beforeApply method is executed : immediately after the corresponding method call on the ViewState and only then . We continue the dive!
In case we have a View:
3) They are alternately proxied to the toggleCheese method.
4) the afterApply method for the ViewCommands class for the given command is called. In it, we get the strategy and call its afterApply method.
However, afterApply is called not only in this case. It will also be called in case of an attachment of a new View. Let's look at this case. When the attachment View is called, the attachView method (View view)
The attachView method (View view) is called from the onAttach () method of the MvpDelegate class. That in turn is called quite often: from the onStart () and onResume () methods. However, the library guarantees that afterApply will be called once for an attachment view (see listing below).
public void attachView(View view) { if (view == null) { throw new IllegalArgumentException("Mvp view must be not null"); } boolean isViewAdded = mViews.add(view); if (!isViewAdded) { return; } mInRestoreState.add(view); Set<ViewCommand<View>> currentState = mViewStates.get(view); currentState = currentState == null ? Collections.<ViewCommand<View>>emptySet() : currentState; restoreState(view, currentState); mViewStates.remove(view); mInRestoreState.remove(view); }
1) View is added to the list.
2) If it is not in this list, I twist it to the StateRestoring state .
.
This makes it possible for activations / views / fragments to understand that the state is being restored. The presenter has an isInRestoreState () method. This mechanism is necessary in order not to perform some actions twice (for example, starting an animation to translate a view into the desired state). This is the only presenter method that returns non- void . This method belongs to the presenter , since view and mvpDelegate can have several presenters and putting them in these classes would lead to collisions.
3) Next, the state is restored
It is worth noting that the afterApply method can be called several times. Pay attention to this when writing your custom strategies.
We got acquainted with how strategies work, it’s time to consolidate skills in practice.
Scheme of work
So, first, let's understand what kind of strategy we want to get. We agree on the notation.
This scheme is similar to the scheme from the first part, but there is an important difference in it — a tag designation appeared. The absence of a command tag means that we did not specify it and it assumed the default value - null
We want to implement the following strategy:
When calling a team presenter (2) with the AddToEndSingleTagStrategy strategy:
When recreating a View:
Implementation
1) Implement the StateStrategy interface. To do this, override the beforeApply and afterApply methods.
2) The implementation of the beforeApply method will be very similar to the implementation of a similar method in the AddToEndSingleStrategy class.
We want to remove from the queue absolutely all teams with this tag, i.e. even teams with a different strategy, but with a similar tag, will be deleted.
Therefore, instead of entry.class == incomingCommand.class, we will use entry.tag == incomingCommand.tag
In Kotlin == equivalent to .equals in Java
We also need to remove the break string, since, unlike AddToEndSingleStrategy , we may have several commands in the queue for deletion.
3) We will leave the implementation of the afterApply method empty, since we do not need to change the queue after applying the command.
So, what we have:
class AddToEndSingleTagStrategy() : StateStrategy { override fun <View : MvpView> beforeApply( currentState: MutableList<ViewCommand<View>>, incomingCommand: ViewCommand<View>) { val iterator = currentState.iterator() while (iterator.hasNext()) { val entry = iterator.next() if (entry.tag == incomingCommand.tag) { iterator.remove() } } currentState.add(incomingCommand) } override fun <View : MvpView> afterApply( currentState: MutableList<ViewCommand<View>>, incomingCommand: ViewCommand<View>) { //Just do nothing } }
That's all, it remains to illustrate how we will use the strategy (see listing below).
interface LaunchView : MvpView { @StateStrategyType(AddToEndSingleStrategy::class, tag = BURGER_TAG) fun setBreadType(breadType: BreadType) @StateStrategyType(AddToEndSingleStrategy::class, tag = BURGER_TAG) fun toggleCheese(enable: Boolean) @StateStrategyType(AddToEndSingleTagStrategy::class, tag = BURGER_TAG) fun clearBurger(breadType: BreadType, cheeseSelected: Boolean) // companion object { const val BURGER_TAG = "BURGER" } }
The full code sample can be viewed in the Moxy repository. I remind you that this is a sample and the solutions in it are purely to illustrate the functionality of the framework
What else can you use custom strategies for:
1) glue the previous teams into one;
2) change the order of command execution if the commands are not commutative (a • b! = B • a);
3) throw out all the commands that do not contain the current tag;
four) ..
If you often use commands, and they are not in the list of defaults - write, we will discuss their addition.
You can discuss Moxy in the community chat.
We are waiting for comments and suggestions on the article and the library;)
(I) hereinafter "we" - the authors of Moxy: Xanderblinov , senneco and all the guys from the community who helped with advice, comments and pullrequest. Full list of contributors can be found here.
Source: https://habr.com/ru/post/341108/
All Articles