В части 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 может вызываться несколько раз. Обратите на это внимание, когда будете писать свои кастомные стратегии.
Мы познакомились с тем, как работают стратегии, пришло время закрепить навыки на практике.
Создаем кастомную стратегию
Схема работы
Итак, для начала давайте поймем, какую именно стратегию мы хотим получить. Договоримся об обозначениях.
Данная схема похожа на схему из первой части, однако в ней есть важное отличие -— появилось обозначение тэга. Отсутствие тэга у команды обозначает, что мы его не указывали и он принял значение по умолчанию — null
Мы хотим реализовать следующую стратегию:
При вызове презентером команды (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
мы хотим, чтобы эти методы были взаимоисключающие, при этом, остальные методы должны лежать в очереди команд. Тут нам поможет кастомная стратегия