![](https://habrastorage.org/webt/vm/l6/wb/vml6wbfns9o_ccut4y4lkbymouk.png)
В части 1 мы разобрались, для чего нужны стратегии в Moxy и в каких случаях уместно применять каждую из них. В этой статье мы рассмотрим механизм работы стратегий изнутри, поймем, в каких случаях нам могут понадобиться кастомные стратегии, и попробуем создать свою собственную.
Зачем нужны кастомные стратегии
Зачем вообще Moxy поддерживает создание
пользовательских стратегий? При проектировании библиотеки мы (I) старались учесть все возможные случаи, и встроенные стратегии практически на сто процентов их покрывают. Однако в некоторых случаях может потребоваться больше власти над ситуацией, и мы не хотели вас ограничивать. Рассмотрим один из таких случаев.
Презентер отвечает за выбор бизнес-ланча, который состоит из бургера и напитка.
Команды в зависимости от функции у нас делятся на следующие типы:
- кастомизируют бургер (добавить/удалить сыр, выбрать ржаную/пшеничную булку и т.д.);
- кастомизируют напиток (выбрать количество ложек сахара, добавить/удалить лимон и т.д);
- оповещают о том, что заказ отправлен.
Итак, мы хотим уметь отдельно управлять очередями команд для бургера и для напитка. Стратегий по умолчанию для этого не хватит, дополнительно нам нужны бургерные и напиточные! Давайте их изобретем, но для начала разберемся, как вообще устроен механизм применения команд и на что влияют стратегии.
Механика работы команд Moxy
Начнем издалека: в конструкторе презентера создается ViewState, все команды проксируются через него. ViewState содержит очередь команд — ViewCommands (класс, который отвечает за список команд и стратегий) и список View. В списке View может содержаться как несколько View, если вы используете презентер типа Global или Weak, так и ни одного (в ситуации, когда у вас фрагмент ушел в бэк стэк).
Между нами говоря, не стройте на их основе архитектуру, так как они залезают не в свою зону ответственности. Шарить данные между экранами лучше при помощи общих сущностей типа интеракторов в чистой архитектуре. О чистой архитектуре есть неплохая статья
Для начала разберемся, что же представляет собой стратегия:
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);
}
Это интерфейс с двумя методами: beforeApply и afterApply. Каждый метод на вход принимает новую команду и текущий список команд, который и будет меняться (или останется без изменений) в теле метода. У каждой команды мы можем получить тэг (это строка, которую можно указать в аннотации StateStrategyType) и тип стратегии (см. листинг ниже). Как именно менять список решаем, опираясь только на эту информацию.
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;
}
}
Давайте поймем, когда у нас будут вызываться данные методы. Итак, у нас есть интерфейс SimpleBurgerView, который умеет только добавлять немного сыра(II).
interface SimpleBurgerView : BaseView {
@StateStrategyType(value = AddToEndSingleStrategy::class, tag = "Cheese")
fun toggleCheese(enable: Boolean)
}
Рассмотрим, что происходит при вызове метода toggleCheese у сгенерированного класса LaunchView$$State (см. листинг):
@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 (см. листинг ниже)
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) Вызывается метод beforeApply для класса ViewCommands для данной команды. В нем мы получаем стратегию и вызываем ее метод beforeApply. (см. листинг ниже)
public void beforeApply(ViewCommand<View> viewCommand) {
StateStrategy stateStrategy = getStateStrategy(viewCommand);
stateStrategy.beforeApply(mState, viewCommand);
}
Ура! Теперь мы знаем, когда выполняется метод beforeApply у стратегии: сразу же после соответствующего вызова метода у ViewState и только тогда. Продолжаем погружение!
В случае если у нас есть View:
3) Им поочередно проксируется метод toggleCheese.
4) Вызывается метод afterApply для класса ViewCommands для данной комнады. В нем мы получаем стратегию и вызываем ее метод afterApply.
Однако afterApply вызывается не только в этом случае. Он также будет вызван в случае аттача новой View. Давайте рассмотрим этот случай. При аттаче View вызывается метод attachView(View view)
Метод attachView(View view) вызывается из метода onAttach() класса MvpDelegate. Тот в свою очередь вызывается довольно часто: из методов onStart() и onResume(). Тем не менее библиотека гарантирует, что afterApply вызовется один раз для приаттаченой вью (см. листинг ниже).
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) Вью добавляется в список.
2) Если ее в этом списке не было, вью переводится в состояние StateRestoring
.
Это даёт возможность активити/вью/фрагменту понять, что состояние восстанавливается. У презентера есть метод isInRestoreState(). Этот механизм необходим для того, чтобы не выполнять некоторые действия дважды (например, старт анимации для перевода вью в нужное состояние). Это единственный метод presenter, который возвращает не void. Данный метод принадлежит presenter, т.к. view и mvpDelegate могут иметь несколько презентеров и помещение их в эти классы привело бы к коллизиям.
3) Далее происходит восстановление состояния
Стоит отметить, что метод afterApply может вызываться несколько раз. Обратите на это внимание, когда будете писать свои кастомные стратегии.
Мы познакомились с тем, как работают стратегии, пришло время закрепить навыки на практике.
Создаем кастомную стратегию
Схема работы
Итак, для начала давайте поймем, какую именно стратегию мы хотим получить. Договоримся об обозначениях.
![](https://habrastorage.org/webt/bl/nu/77/blnu77r8v8fpcmmfcub_nsj343w.png)
Данная схема похожа на схему из первой части, однако в ней есть важное отличие -— появилось обозначение тэга. Отсутствие тэга у команды обозначает, что мы его не указывали и он принял значение по умолчанию — null
Мы хотим реализовать следующую стратегию:
![](https://habrastorage.org/webt/pv/lh/vd/pvlhvdcjk6sq4x425zcdl0tbhye.png)
При вызове презентером команды (2) со стратегией AddToEndSingleTagStrategy:
- Команда (2) добавляется в конец очереди ViewState.
- В случае, если в очереди уже находилась любая другая команда с аналогичным тэгом, она удаляется из очереди.
- Команда (2) применяется ко View, если оно находится в активном состоянии.
При пересоздании View:
- Ко View последовательно применяются команды из очереди ViewState
Реализация
1) Реализуем интерфейс StateStrategy. Для этого переопределяем методы beforeApply и afterApply
2) Реализация метода beforeApply будет очень похожа на реализацию аналогичного метода в классе AddToEndSingleStrategy.
Мы хотим удалить из очереди абсолютно все команды с данным тегом, т.е. удаляться будут даже команды с другой стратегией, но аналогичным тегом.
Поэтому мы вместо строчки entry.class == incomingCommand.class будем использовать entry.tag == incomingCommand.tag
В Kotlin == равносильно .equals в Java
Также нам необходимо убрать строку break, так как в отличие от AddToEndSingleStrategy у нас в очереди могут появиться несколько команд для удаления.
3) Реализацию метода afterApply оставим пустой, так как у нас нет необходимости менять очередь после применения команды.
Итак, что у нас получилось:
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
}
}
Вот и все, осталось проиллюстрировать, как мы будем использовать стратегию (см. листинг ниже).
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"
}
}
Полный пример кода можно посмотреть в репозитории Moxy. Напоминаю, что это сэмпл и решения в нем приведены сугубо для иллюстрации функционала фреймворка
Предназначение..
Для чего еще можно использовать кастомные стратегии:
1) склеивать предыдущие команды в одну;
2) менять порядок выполнения команд, если команды не коммутативны (a•b != b•a);
3) выкидывать все команды, которые не содержат текущий tag;
4) ..
Если вы часто используете команды, а их нет в списке дефолтных — пишите, обсудим их добавление.
Обсудить Moxy можно в чате сообщества
Ждем замечаний и предложений по статье и библиотеке ;)
(I) здесь и далее "мы" — авторы Moxy: Xanderblinov, senneco и все ребята из сообщества, которые помогали советами, замечаниями и пулреквестами. Полный список контрибьютеров можно посмотреть здесь
(II) Код в листингах написан на Kotlin
dmdev
Можете привести пример из жизни, когда стандартными стратегиями не обойтись?
Xanderblinov Автор
Довольно часто встречается ситуация: На экране есть состояния: LOADING, DATA, STUB.
Соответсвенно, у нас будут методы у SomeView
мы хотим, чтобы эти методы были взаимоисключающие, при этом, остальные методы должны лежать в очереди команд. Тут нам поможет кастомная стратегия