Стратегии в Moxy (Часть 2)

в 10:12, , рубрики: android architecture, moxy, mvp, redmadrobot, Блог компании REDMADROBOT, Разработка под android

Стратегии в Moxy (Часть 2) - 1

В части 1 мы разобрались, для чего нужны стратегии в Moxy и в каких случаях уместно применять каждую из них. В этой статье мы рассмотрим механизм работы стратегий изнутри, поймем, в каких случаях нам могут понадобиться кастомные стратегии, и попробуем создать свою собственную.

Зачем нужны кастомные стратегии

Зачем вообще Moxy поддерживает создание
пользовательских стратегий? При проектировании библиотеки мы (I) старались учесть все возможные случаи, и встроенные стратегии практически на сто процентов их покрывают. Однако в некоторых случаях может потребоваться больше власти над ситуацией, и мы не хотели вас ограничивать. Рассмотрим один из таких случаев.

Презентер отвечает за выбор бизнес-ланча, который состоит из бургера и напитка.
Команды в зависимости от функции у нас делятся на следующие типы:

  • кастомизируют бургер (добавить/удалить сыр, выбрать ржаную/пшеничную булку и т.д.);
  • кастомизируют напиток (выбрать количество ложек сахара, добавить/удалить лимон и т.д);
  • оповещают о том, что заказ отправлен.

Итак, мы хотим уметь отдельно управлять очередями команд для бургера и для напитка. Стратегий по умолчанию для этого не хватит, дополнительно нам нужны бургерные и напиточные! Давайте их изобретем, но для начала разберемся, как вообще устроен механизм применения команд и на что влияют стратегии.

Механика работы команд Moxy

Начнем издалека: в конструкторе презентера создается ViewState, все команды проксируются через него. ViewState содержит очередь команд — ViewCommands (класс, который отвечает за список команд и стратегий) и список View. В списке View может содержаться как несколько View, если вы используете презентер типа Global или Weak, так и ни одного (в ситуации, когда у вас фрагмент ушел в бэк стэк).

Global и Weak презентеры

Между нами говоря, не стройте на их основе архитектуру, так как они залезают не в свою зону ответственности. Шарить данные между экранами лучше при помощи общих сущностей типа интеракторов в чистой архитектуре. О чистой архитектуре есть неплохая статья

Для начала разберемся, что же представляет собой стратегия:

StateStrategy

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) и тип стратегии (см. листинг ниже). Как именно менять список решаем, опираясь только на эту информацию.

ViewCommand

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 (см. листинг):

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 (см. листинг ниже)

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. (см. листинг ниже)

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 вызовется один раз для приаттаченой вью (см. листинг ниже).

MvpViewState.attachView

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

Зачем нужно состояние StateRestoring

.
Это даёт возможность активити/вью/фрагменту понять, что состояние восстанавливается. У презентера есть метод isInRestoreState(). Этот механизм необходим для того, чтобы не выполнять некоторые действия дважды (например, старт анимации для перевода вью в нужное состояние). Это единственный метод presenter, который возвращает не void. Данный метод принадлежит presenter, т.к. view и mvpDelegate могут иметь несколько презентеров и помещение их в эти классы привело бы к коллизиям.

3) Далее происходит восстановление состояния

Стоит отметить, что метод afterApply может вызываться несколько раз. Обратите на это внимание, когда будете писать свои кастомные стратегии.

Мы познакомились с тем, как работают стратегии, пришло время закрепить навыки на практике.

Создаем кастомную стратегию

Схема работы
Итак, для начала давайте поймем, какую именно стратегию мы хотим получить. Договоримся об обозначениях.

Стратегии в Moxy (Часть 2) - 2

Данная схема похожа на схему из первой части, однако в ней есть важное отличие -— появилось обозначение тэга. Отсутствие тэга у команды обозначает, что мы его не указывали и он принял значение по умолчанию — null

Мы хотим реализовать следующую стратегию:

Стратегии в Moxy (Часть 2) - 3

При вызове презентером команды (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 пользователей

В 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

Автор: Александр Блинов

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js