На этой схеме не скелет древнего обитателя водных глубин и не схема метро какого-то мегаполиса, это карта переходов по экранам вполне реального Андроид приложения! Но, несмотря на сложность, нам удалось её удачно реализовать, а решение оформить в виде небольшой библиотеки, о которой и пойдет речь в статье.



Чтобы заранее избежать вопросов о названии, уточню: Cicerone ("чи-че-ро?-не") – устаревшее слово с итальянскими корнями, со значением «гид для иностранцев».


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


Так как я в этом плане предпочитаю MVP, то далее по тексту будет часто встречаться слово «презентер», но хочу отметить, что представленное решение никак не ограничивает вас в выборе архитектуры (можно даже использовать в классическом подходе «все во Fragment’ах», и даже в этом случае Cicerone даст свой профит!).


Навигация – это скорее бизнес-логика, поэтому ответственность за переходы я предпочитаю возлагать на презентер. Но в Андроиде не все так гладко: для осуществления переходов между Activity, переключения Fragment’ов или смены View внутри контейнера


  1. не обойтись без зависимости от Context’a, который не хочется передавать в слой логики, связывая тем самым его с платформой, усложняя тестирование и рискуя получить утечки памяти (если забыть очистить ссылку);
  2. надо учитывать жизненный цикл контейнера (например, java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState у Fragment’ов).

Поэтому и появилось решение, реализованное в Cicerone.
Начать, думаю, стоит со структуры.


Структура



На схеме есть четыре сущности:


  • Command – это простейшая команда перехода, которую выполняет Navigator.
  • Navigator – непосредственная реализация «переключения экранов» внутри контейнера.
  • Router – это класс, который превращает высокоуровневые вызовы навигации презентера в набор Command.
  • CommandBuffer – отвечает за сохранность вызванных команд навигации, если в момент их вызова нет возможности осуществить переход.

Теперь о каждой подробнее.


Команды переходов


Мы заметили, что любую карту переходов (даже достаточно сложную, как на первом изображении) можно реализовать, используя четыре базовых перехода, комбинируя которые, мы получим необходимое поведение.


Forward



Forward (String screenKey, Object transitionData) – команда, которая осуществляет переход на новый экран, добавляя его в текущую цепочку экранов.
screenKey – уникальный ключ, для каждого экрана.
transitionData – данные, необходимые новому экрану.


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


Back



Back() – команда, удаляющая последний активный экран из цепочки, и возвращающая на предыдущий. При вызове на корневом экране ожидается выход из приложения.


BackTo



BackTo(String screenKey) – команда, позволяющая вернуться на любой из экранов в цепочке, достаточно указать его ключ. Если в цепочке два экрана с одинаковым ключом, то выбран будет последний (самый «правый»).


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


На практике эта команда очень удобна. Например, для авторизации: два экрана. Телефон -> СМС, а потом выход на тот, с которого была запущена авторизация.

Replace



Replace (String screenKey, Object transitionData) – команда, заменяющая активный экран на новый.
Кто-то может возразить, что этого результата удастся достичь, вызвав подряд команды Back и Forward, но тогда на корневом экране мы выйдем из приложения!


Вот и всё! Этих четырёх команд на практике достаточно для построения любых переходов. Но есть ещё одна команда, которая не относится к навигации, однако очень полезна на практике.


SystemMessage



SystemMessage (String message) – команда, отображающая системное сообщение (Alert, Toast, Snack и т. д.).


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


Все команды отмечены интерфейсом-маркером Command. Если вам по какой-то причине понадобилась новая команда, просто создайте её, никаких ограничений!


Команды сами по себе не реализуют переключение экранов, а только описывают эти переходы. За их выполнение отвечает Navigator.


public interface Navigator {
  void applyCommand(Command command);
}

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


  • В Activity для переключения Fragment’ов.
  • Во Fragment’е для переключения вложенных (child) Fragment’ов.
  • … ваш вариант.

Так как в подавляющем большинстве Андроид приложений навигация опирается на переключение Fragment’ов внутри Activity, чтобы не писать однотипный код, в библиотеке уже есть готовый FragmentNavigator (и SupportFragmentNavigator для SupportFragment’ов), реализующий представленные команды.


Достаточно:


1) передать в конструктор ID контейнера и FragmentManager;
2) реализовать методы выхода из приложения и отображения системного сообщения;
3) реализовать создание Fragment’ов по screenKey.


За более подробным примером советую заглянуть в Sample-приложение.

В приложении необязательно должен быть один Navigator. Пример (тоже реальный, кстати): в Activity есть BottomBar, который доступен для пользователя ВСЕГДА. Но в каждом табе есть собственная навигация, которая сохраняется при переключении табов в BottomBar’е.


Решается это одним навигатором внутри Activity, который переключает табы, и локальными навигаторами внутри каждого Fragment’а-таба.
Таким образом, каждый отдельный презентер не завязан на то, где он находится: внутри цепочки одного из табов или в отдельном Activity. Достаточно предоставить ему правильный Router. Один Router связан только с одним Navigator’ом в любой момент времени. Об этом чуть дальше.

Router


Как было сказано выше, комбинируя команды, можно реализовать любой переход. Именно этой задачей и занимается Router.


Например, если стоит задача по некоторому событию в презентере:


1) скинуть всю цепочку до корневого экрана;
2) заменить корневой экран на новый;
3) и еще показать системное сообщение;


то в Router добавляется метод, который передает последовательность из трёх команд на выполнение в CommandBuffer:


public void navigateToNewRootWithMessage(String screenKey,
                               Object data,
                               String message) {
  executeCommand(new BackTo(null));
  executeCommand(new Replace(screenKey, data));
  executeCommand(new SystemMessage(screenKey, data));
}

Если бы презентер сам вызывал эти методы, то после первой команды BackTo(), он был бы уничтожен (не совсем так, но суть передаёт) и не завершил работу корректно.

В библиотеке есть готовый Router, используемый по-умолчанию, с самыми необходимыми переходами, но как и с навигатором, никто не запрещает создать свою реализацию.


navigateTo() – переход на новый экран.
newScreenChain() – сброс цепочки до корневого экрана и открытие одного нового.
newRootScreen() – сброс цепочки и замена корневого экрана.
replaceScreen() – замена текущего экрана.
backTo() – возврат на любой экран в цепочке.
exit() – выход с экрана.
exitWithMessage() – выход с экрана + отображение сообщения.
showSystemMessage() – отображение системного сообщения.


CommandBuffer


CommandBuffer – класс, который отвечает за доставку команд навигации Navigator’у. Логично, что ссылка на экземпляр навигатора хранится в CommandBuffer’е. Она попадает туда через интерфейс NavigatorHolder:


public interface NavigatorHolder {
  void setNavigator(Navigator navigator);
  void removeNavigator();
}

Кроме того, если в CommandBuffer поступят команды, а в данный момент он не содержит Navigator’а, то они сохранятся в очереди, и будут выполнены сразу при установке нового Navigator’а. Именно благодаря CommandBuffer’у удалось решить все проблемы жизненного цикла.


Конкретный пример для Activity:


@Override
protected void onResume() {
  super.onResume();
  SampleApplication.INSTANCE.getNavigatorHolder().setNavigator(navigator);
}

@Override
protected void onPause() {
  SampleApplication.INSTANCE.getNavigatorHolder().removeNavigator();
  super.onPause();
}

Почему именно onResume и onPause? Для безопасной транзакции Fragment’ов и отображения системного сообщения в виде алерта.

От теории к практике. Как использовать Cicerone?


Предположим, мы хотим реализовать навигацию на Fragment’ах в MainActivity:
Добавляем зависимость в build.gradle


repositories {
    maven {
        url 'https://dl.bintray.com/terrakok/terramaven/'
    }
}

dependencies {
    //Cicerone
    compile 'ru.terrakok.cicerone:cicerone:1.0'
}

В классе SampleApplication инициализируем готовый роутер


public class SampleApplication extends Application {
    public static SampleApplication INSTANCE;
    private Cicerone<Router> cicerone;

    @Override
    public void onCreate() {
        super.onCreate();
        INSTANCE = this;
        cicerone = Cicerone.create();
    }

    public NavigatorHolder getNavigatorHolder() {
        return cicerone.getNavigatorHolder();
    }

    public Router getRouter() {
        return cicerone.getRouter();
    }
}

В MainActivity создаем навигатор:


private Navigator navigator = new SupportFragmentNavigator(getSupportFragmentManager(),
                                                           R.id.main_container) {
  @Override
  protected Fragment createFragment(String screenKey, Object data) {
    switch(screenKey) {
      case LIST_SCREEN:
    return ListFragment.getNewInstance(data);
      case DETAILS_SCREEN:
    return DetailsFragment.getNewInstance(data);
      default:
        throw new RuntimeException(“Unknown screen key!”);
    }
  }

  @Override
  protected void showSystemMessage(String message) {
    Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show();
  }

  @Override
  protected void exit() {
    finish();
  }
};

@Override
protected void onResume() {
  super.onResume();
  SampleApplication.INSTANCE.getNavigatorHolder().setNavigator(navigator);
}

@Override
protected void onPause() {
  super.onPause();
  SampleApplication.INSTANCE.getNavigatorHolder().removeNavigator();
}

Теперь из любого места приложения (в идеале из презентера) можно вызывать методы роутера:


SampleApplication.INSTANCE.getRouter().backTo(...);

Частные случаи и их решение


Single Activity?


Нет! Но Activity я не рассматриваю как экраны, только как контейнеры. Смотрите: Router создан в классе Application, поэтому при переходе с одного Activity на другое, просто будет меняться активный навигатор, поэтому вполне можно делить приложение на независимые Activity, внутри которых будут уже переключения экранов. Конечно, стоит понимать, что цепочки экранов в таком случае будут привязаны к отдельным Activity, и команда BackTo() сработает только в контексте одного Activity.


Вложенная навигация


Я выше приводил пример, но повторюсь снова:


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


Решается это двумя типами навигации: глобальной и локальной.


GlobalRouter – роутер приложения, связанный с навигатором Activity.
Презентер, обрабатывающий клики по табам, вызывает команды у GlobalRouter.


LocalRouter – роутеры внутри каждого Fragment’а-контейнера. Навигатор для LocalRouter'а реализует сам Fragment-контейнер.
Презентеры, относящиеся к локальным цепочкам внутри табов, получают для навигации LocalRouter.


Где связь? Во Fragment’ах-контейнерах есть доступ и к глобальному навигатору! В момент, когда локальная цепочка внутри таба закончилась и вызвана команда Back(), то Fragment передает её в глобальный навигатор.


Совет: для настройки зависимостей между компонентами, используйте Dagger 2, а для управления их жизненным циклом – его CustomScopes.

А что с системной кнопкой Back?


Этот вопрос специально не решается в библиотеке. Нажатие на кнопку Back надо воспринимать как взаимодействие пользователя и передавать просто как событие в презентер.


Но есть ведь Flow или Conductor?


Мы смотрели другие решения, но отказались от них, так как одной из главных задач было использовать максимально стандартный подход и не создавать очередной фреймворк со своим FragmentManager’ом и BackStack’ом.


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


Во-вторых, не придется всецело полагаться на сложные сторонние решения, что чревато затрудненной поддержкой.


Итог


Библиотека Cicerone:


  • не завязана на Fragment’ы;
  • не фреймворк;
  • предоставляет короткие вызовы;
  • легка в расширении;
  • приспособлена для тестов;
  • не зависит от жизненного цикла!

GitHub


Cicerone is a lightweight library that makes the navigation in an Android app easy.


Поделиться с друзьями
-->

Комментарии (27)


  1. Firsto
    20.11.2016 16:08

    Только что были на вашем докладе «Навигация без боли и слёз» на DevFest в Новосибирске. Библиотека оказалась весьма интересной, очень понравился такой достаточно прозрачный поход к навигации. Возвращаемся домой, внедрять Cicerone в свои проекты. Спасибо, что делаете нашу жизнь лучше!


    1. terrakok
      20.11.2016 16:25

      Рад, что заинтересовал вас!


  1. Strax
    21.11.2016 11:46

    Добрый день, доклад действительно за интересовал, оданако появилось несколько вопросов:
    1) Ок, команда SystemMessage однозначно решала какие-то ваши проблемы в проекте, однако встает 2 вопроса 1: насколько это вообще зона ответственности навигатора 2: если все же его, то почему насколько скудно, тогда уж надо разрешать ему в принципе разруливать логику startActivityForResult, то есть позволять перебрасывать некоторые объекты между экранами.

    2) Мне очень понравилась Ваша идея разруливать навигацию в стороне от вьюх, однако (огромное) ограничение, связаное с тем что Activity это не экран, ломает всю красоту (на мой взгляд), хотелось бы единообразно работать с любым стеком навигаций в не зависимости от того с чем ты работаешь (Activity, Fragment and so on), а так получается что фрагменты и Android View используют Ваш подход, а Activity нет. Не могли бы Вы рассказать в чем причина? Почему Активити это не экран?


    1. terrakok
      21.11.2016 12:29

      Про команду SystemMessage.
      Я специально выделил ей особое внимание, так как она только косвенно связана с навигацией. Можно обойтись и без нее, но в ряде случаев это будет не так изящно как с ней!
      Пример: форма оплаты. Пользователь заполнил поля и нажал далее, но сервер вернул ошибку авторизации. Нам надо сказать об этом пользователю и перекинуть на экран входа. Без команды SystemMessage это можно сделать двумя способами:


      • на экране оплаты показать диалог и ждать нажатия кнопки ОК. Тут придется блокировать отмену этого диалога + очищать поля с секретной информацией, так как ошибка авторизрции может говорить о том, что приложение в чужих руках.
      • переходить на экран входа с дополнительной информацией о том, что надо при запуске показать сообщение. Это само по себе некрасиво, так как, по логике, экран входа не должен ничего иного делать, кроме как регистрировать пользователя.

      Разруливание логики startActivityForResult оставлено на решение в частном порядке, так как эта логика доступна только в Activity и частично во Fragment'ах, а библиотека не делает разницы между разными видами View. Как я и говорил, Cicerone легко расширять, все в ваших руках.
      Важный момент: в подходе MVP не предполагается обмена данными между View и даже между Presenter'ами. Это надо реализовывать через модель. Да, в андроиде Activity обладают механизмом startActivityForResult, но он не единственно возможный. Если речь о сторонних библиотеках или системных Activity, то я советую рассматривать результат запуска таких Activity как обычное пользовательское действие и передавать данные в свой презентер, где уже делать остальную обработку.


      "Activity это не экран" — кто сказал? Activity может быть View без каких либо ограничений! После перехода на новое Activity, первое Activity очистит навигатор в методе onPause, а новое Activity установит свой навигатор в onResume. Здесь будет только один момент, обусловленный архитектурой андроида: Activity будет, с одной стороны, реализовывать View, а с другой, заниматься навигацией. (помните слайд с Франкенштейном?)


    1. terrakok
      21.11.2016 23:37

      Я добавил в Sample приложение пример навигации на Activity. Welcome!


      1. Xanderblinov
        23.11.2016 21:52

        Без команды SystemMessage это можно сделать двумя способами

        А еще есть третий способ, презентер, на который возвращаемся, подписывается на ивенты/обсервер модели о протухании авторизации.

        Таким образом, при возврате на экран он примет состояние: показать ошибку авторизации


        1. terrakok
          23.11.2016 22:22

          А если это другая ошибка? Тогда нам надо все презентеры подписать на все ошибки? Я же говорю, решить можно по-разному, но SystemMessage поможет в некоторых случаях сделать это проще и изящнее.


          Еще хочу подчеркнуть важную деталь: SystemMessage создана для показа сообщений именно при навигации, то есть это можно рассматривать как некоторое состояние в навигации. Неправильно использовать эту команду просто для показа сообщений на экранах. Это ответственность Вью. SystemMessage не умеет обрабатывать клик или еще что-то, это только сообщение пользователю о случившемся переходе.


  1. evnik
    23.11.2016 02:18

    Этих четырёх команд на практике достаточно для построения любых переходов

    Не очень понятно что делать, если после успешной аунтефикации мне нужно переходить на новый экран, а не возвращаться на предыдущий? Или как запретить возвращаться на один из предыдущих экранов (не запрещая переход к корневому)? Команда типа RemoveBackScreen(String screenKey) могла бы решить обе эти задачи, а также с ней можно реализовать команду Replace.


    1. terrakok
      23.11.2016 02:26

      1) Если я вас правильно понял, то вы спрашиваете про такой случай:
      до перехода (F — активный экран)
      a -> b -> c -> d -> e -> F
      после перехода (G — активный экран)
      a -> b -> c -> d -> G
      Это решается методом в роутере:


      public void customTransition() {
        backTo("d");
        forward("g");
      }

      2) Запрещать возвращаться на экран? Это как? Зачем тогда его оставлять в цепочке?


      1. evnik
        23.11.2016 04:13

        1) Да. Так можно решить, но хотелось бы избавиться от перехода назад к d. Мы ведь на самом деле не хотим его отображать. Кроме того, откуда нам знать, что там d? У нас нет ограничений на место откуда запущена аутентификация.


        2) Не надо оставлять его в цепочке. Но убрать нужно уже после того, как он оказался в ее середине. Абстрактный пример (не очень хороший дизайн, просто для примера):


        1. Зайти в профиль контакта
        2. Зайти в группу контактов
        3. Зайти в другой профиль
        4. Зайти в группу контактов
          ...
        5. Зайти опять в профиль контакта из пункта 1
        6. Удалить контакт
        7. Возвращаться назад до корневого экрана

        При возврате в экран из пункта 1 показывать будет нечего (контакт уже удален). Хорошо бы удалить этот экран из цепочки сразу при удалении контакта.


        1. terrakok
          23.11.2016 11:03

          1) Экран D показан не будет, так как смена экранов происходит быстрее. И все зависит от задачи. Можно сделать возврат не к конкретному экрану, а "на два назад", если экраны аутентификации попадают в общую цепочку. Такие случаи решаются расширением роутера. Для этого библиотека и создавалась гибкой.
          Другой вариант: делать аутентификацию в отдельном активити с локальной цепочкой экранов.


          2) Вы описываете типичный кейс бизнес логики. Если вы хотите, можно создать для таких случаев отдельную команду навигации. Повторюсь, что все для этого готово. Библиотека включает в себя необходимый минимум, если стараться учесть все кейсы, то маленькое решение превратится в гигантского монстра!


          Возможно в будущем мы и добавим что-то еще, но на данный момент это не кажется критичным


          1. evnik
            23.11.2016 22:12

            Библиотека включает в себя необходимый минимум, если стараться учесть все кейсы, то маленькое решение превратится в гигантского монстра!

            Все верно. Просто по моему мнению, вместо команды Replace хорошо бы иметь что-то вроде RemoveBackScreen. Библиотека останется маленькой, но станет более гибкой.


            1. terrakok
              23.11.2016 22:26

              RemoveBackScreen не решить явным образом при навигации по Активити.


              А если чрезмерная гибкость может создать неоправданные сложности, то лучше ограничится на Replace.


              Попробуйте сделать RemoveBackScreen, а там посмотрим :)


              1. evnik
                23.11.2016 22:57

                Делали что-то такое в навигации Appercode, но там, конечно, вся эта навигация гвоздями прибита и все попроще. Cicerone — это более гибкое решение и мне это нравится.


    1. Jeevuz
      23.11.2016 13:46

      Этих четырёх команд на практике достаточно для построения любых переходов

      Тут скорее имелось в виду, что их достаточно для построения практически любых переходов.


      Конечно, может появиться какой-то специфический кейс, который будет удобнее решить по-другому.
      И для этого можно всегда легко добавить свою команду. И сделать свой вариант роутера, в котором добавить переход, использующий эту команду или комбинирующий её с остальными. Но сперва надо посмотреть нельзя ли решить этот кейс встроенными командами (мы же все ленивые).


      Для меня самое прекрасное в либе как раз то, что она маленькая и гибкая.


  1. evnik
    23.11.2016 22:37

    не обойтись без зависимости от Context’a, который не хочется передавать в слой логики… рискуя получить утечки памяти (если забыть очистить ссылку)

    А как можно получить утечку памяти, если Context все равно живет всегда? Или речь идет об Activity, а не о Context.getApplicationContext?


    1. terrakok
      23.11.2016 23:39

      Речь о контексте необходимом для навигации, например Activity, у которого брать FragmentManager и так далее


  1. Axel_Vasilyev
    24.11.2016 15:06

    Спасибо за отличное решение. Очень понравился подход к навигации. В процессе тестирования возникла одна проблема. Ваш пример использует библиотеку Moxy для реализации mvp. В связи с этим получается нестыковка такого плана, что Moxy повторяет команды для новой view, присоединяющейся к presenter. Команды по переключению фрагментов реализовал с помощью Вашей библиотеки. Но при пересоздании активити они не повторяются, т.е. мы теряем преимущество Moxy с их очередью команд.
    Попытка переопределить Router и добавить очередь не увенчалась успехом, потому что, как я думаю, переопределять нужно ещё и CommandBuffer, именно в нем логично добавить очередь команд и повторять её.
    А также необходимо очищать очередь в случае смены rootScreen.
    Можете что-нибудь посоветовать?


    1. terrakok
      24.11.2016 15:12

      Cicerone решает проблему передачи навигационных вызовов, но не сохранения стека навигации. Cicerone отвечает за то, что команды навигации гарантировано дойдут до навигатора, но не сохраняет их для полного восстановления состояния приложения. (представьте как восстановить цепочку из смеси Activity и Fragment'ов)
      Мы не хотели писать свой FragmentManager + ActivityStack + и тд.
      У себя мы тоже используем Moxy. А за восстановление навигации отвечает сам андроид.


      1. Axel_Vasilyev
        24.11.2016 15:27

        В том и дело, что андроид не верно восстанавливает навигацию в некоторых случаях.
        Попробую объяснить, что я пытаюсь сказать.
        Мы исходим из того, что у нас есть уже реализованные простые команды. (С помощью них действительно можно реализовать большую кучу сценариев)
        Я подумывал о том, каждая из команд может быть двух типов: 1) корневая команда (например, newRootScreen и newScreenChain) — она должна очищать очередь команд, т.к. предыдущие команды переходов бессмысленно повторять; 2) простая команда (например, navigateTo) — она просто заноситься в очередь.
        Таким образом, мы получаем четкую очередь команд до корня и может даже нет смысла повторять их все при переподключении view к presenter, но мне кажется их стоит сохранять.
        Допустим при нажатии назад, мы сможем подставить нужную команду из очереди.


        1. terrakok
          24.11.2016 15:41

          Ничего вам не мешает хранить команды. Они все приходят в навигатор, здесь можно придумать какой-то массив команд и складывать его в бандл при умирании.
          Такие вещи могут превратить библиотеку во фреймворк, то есть внутри будет много собственной логики о которой надо будет постоянно помнить, а кому-то она может не подойти и тогда придется опять переделывать.


    1. Jeevuz
      24.11.2016 15:34

      Но при пересоздании активити они не повторяются, т.е. мы теряем преимущество Moxy с их очередью команд.

      Что-то не совсем понятно, что вы имеете в виду, можете описать иначе?


      Попытка переопределить Router и добавить очередь.

      Очередь невыполненных команд живет в CommandBuffer. Для чего вам очередь в рутере?


      А также необходимо очищать очередь в случае смены rootScreen.

      А для чего?


      Из того что я понял, вы пытаетесь восстановить состояние стека экранов при помощи Cicerone, а состояние экрана средствами Moxy. Но зачем вам это если FragmentManager и так восстановит состояние бекстека я не понимаю. Или вы делаете без фрагментов?


      1. Axel_Vasilyev
        24.11.2016 15:45

        1) у меня есть активити и несколько фрагментов (переключение из BottomBar), при повороте экрана Moxy следит, чтобы была выделена нужная иконка фрагмента, а фрагмент может загрузиться не тот (команда открытия нужного фрагмента не повторяется)

        2) я пытался унаследоваться от Router, чтобы добавить очередь, адекватного ничего не вышло. Очередь нужно добавлять в класс CommandBuffer. Там только очередь НЕВЫПОЛНЕННЫХ команд, а мне нужно повторить ВЫПОЛНЕННЫЕ при повороте экрана.

        3) пока делаю с фрагментами, но в будущем может будет и без них. я просто предлагаю логику для реализации очереди


        1. Jeevuz
          24.11.2016 16:05
          +1

          1) Думаю тут проблема не в либе и не в том, что надо восстанавливать команды. Не надо. Нужно разобраться почему не восстанавливается нужный фрагмент. Он должен восстанавливаться силами FragmentManager и android. Ищите проблему в своей реализации. Потому что то, о чем говорите вы все верно отрабатывает в sample-приложении. Можно уйти далеко по стеку фрагментов и повернуть и все восстановится. Силами cистемы. Даже при включенном "не сохранять действия".


          2) Я понимаю, что вы хотели сделать. Реализовать сохранение состояния стека экранов сохраняя последовательность навигационных команд. Как Moxy cохраняет состояние вью. Но это задача для отдельной либы или форка. Мы же наоборот хотели, чтобы либа была простой. Если нужно что-то более сложное — посмотрите в сторону Flow и Conductor.


          3) Библиотека не держит очередь в себе. Это было изначальной мыслью при ее создании. Она дает интерфейс команд, механизм их комбинирования и пересылки. Все. Реализовать команды, хнанение стека экранов, восстановление стека — это все задачи за переделом ответственности библиотеки. В случае с фрагментами все уже сделано в андроиде, а мы даем дефолтную имлементацию навигатора. В случае с вьюхами это надо будет все делать самостоятельно. Либо силами другой либы.


          1. Axel_Vasilyev
            24.11.2016 21:26

            Я Вас понял. Спасибо за ответ. Поищу ошибки в реализации. Возможно посмотрю в сторону других решений.
            Но Ваш проект определенно заслуживает внимания.


            1. Jeevuz
              25.11.2016 10:30

              Спасибо за такую высокую оценку! Очень приятно.
              А вам быстро разобраться, в чем проблема и приятного кодинга!


  1. terrakok
    28.11.2016 17:35

    Для всех интересующихся: добавил в Sample приложение экран со сложной параллельной навигацией (в каждом табе свой стек экранов).