Cicerone android kotlin fragments

Cicerone android kotlin fragments

Буквально год назад коллеги по работе посоветовали мне 2 хорошие библиотеки, по их словам они «должны облегчить мою жизнь». Я посмотрел и поначалу отнесся к ним скептически, т.к. навигацию я уже более-менее сам писать умел, также был немного знаком с MVVM. И всё же, начался новый проект, и была поставлена задача протестировать на нем связку Moxy + Cicerone.

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

Немного о Moxy

Moxy — это реализация паттерна MVP для Android. MVP — это способ разделения ответственности в приложениях. Я не буду описывать как происходит взаимодействие между компонентами MVP в теории, это можно посмотреть в Wikipedia, и более того, большая часть разработчиков уже это знают. Важно то, что в Android всё усложняется пересозданием Activity при изменении любого параметра конфигурации (локализация, ориентация и размер экрана, размер шрифта и другие), и если это изменение происходит во время работы приложения, то состояние Activity необходимо где-то хранить. Есть встроенные способы сохранения состояния через переопределение методов onSaveInstanceState и onRestoreInstanceState, но зачастую этот метод нарушает архитектуру приложения. Разработчики Moxy сделали всё, чтобы нам можно было не думать о сохранении состояния Activity или Fragment. Взглянем на схему:

Как видно, здесь не совсем обычный MVP, т.к. есть четвертый компонент: ViewState. Именно он отвечает за сохранение состояние View после пересоздания. Также ViewState позволяет одному Presenter обслуживать несколько View одновременно или по очереди. Как видно из схемы, новое View, которое присоединилось к Presenter позже первого, получило то же самое состояние, что и первая View. В некоторых случаях это очень удобно.

Если данная схема кажется вам очень сложной, то это только поначалу. На практике становится всё понятно. Взгляните на код:

Немного о Cicerone

Название никак не связано с древнеримским политическим деятелем. Скорее оно связано со значением старого итальянского слова «чи-че-ро-не«, что означало «гид для иностранцев«.

Если подходить к навигации с точки зрения паттерна, то это больше бизнес-логика. Но чтобы открыть Activity в Android, как минимум нужен Context, и передача его в сущность, занимающуюся бизнес-логикой (presenter, viewModel, и т.д.) — это анти-паттерн. А если вы используете архитектуру Single Activity, то для переключения фрагментов требуется учитывать жизненный цикл контейнера (отсылка к java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState у фрагментов).

Решение от Cicerone полностью устраняет эти проблемы. Чтобы понять, как это работает, взглянем на структуру:

Navigator — непосредственная реализация переключения экранов;

Command — команда перехода, которую выполняет Navigator;

CommandBuffer — менеджер, осуществляющий доставку команд навигатору, а также хранение, если навигатор по определенным причинам не может их выполнить в момент поступления;

NavigatorHolder — посредник между Navigator и CommandBuffer (не указан на схеме);

Router — объект, генерирующий низкоуровневые команды для Navigator посредством вызова высокоуровневых методов разработчиком для осуществления навигации;

Screen — высокоуровневая реализация конкретного экрана.

Изначально Cicerone имеет 4 базовые команды переходов:

  • Back()— удаляет текущий экран из стека и делает активным предыдущий экран;
  • BackTo(Screen) — возвращает на указанный экран, если он есть в цепочке, и удаляет все экраны, что были впереди. Если указанный экран не будет найден в стеке или вместо экрана передать null, то переход осуществится на корневой (root) экран;
  • Forward(Screen) — добавляет экран в стек и делает его активным;
  • Replace(Screen) — заменяет активный экран на указанный.
Читайте также:  Android ssh port forwarding

Из этих команд в классе Router сформированы шесть базовых высокоуровневых команд:

  1. navigateTo(Screen)— переход на новый экран;
  2. newScreenChain(Screen) — сброс стека до корневого экрана и открытие одного нового;
  3. newRootScreen(Screen) — сброс стека и замена корневого экрана;
  4. replaceScreen(Screen) — замена активного экрана;
  5. backTo(Screen) — возврат на любой экран в стеке;
  6. exit()— выход с активного экрана.

Оба этих списка могут быть расширены под конкретные нужды разработчика. В первом случае нужно реализовать интерфейс Command, во втором — расширить класс Router.

Чтобы добавить анимацию переходов между экранами, нужно либо полностью реализовать интерфейс Navigator, либо переопределить у базового экземпляра SupportAppNavigator метод setupFragmentTransaction, в котором в качестве параметра имеется экземпляр класса FragmentTransaction. Именно ему нужно добавить либо Transition, либо Animation.

Предлагаю посмотреть небольшой пример использования Cicerone в связке с Moxy:

Источник

Cicerone — простая навигация в Андроид приложении

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

Чтобы заранее избежать вопросов о названии, уточню: 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() – команда, удаляющая последний активный экран из цепочки, и возвращающая на предыдущий. При вызове на корневом экране ожидается выход из приложения.

BackTo

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

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

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

Replace

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

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

SystemMessage

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

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

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

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

В зависимости от задачи, 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:

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

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

Читайте также:  Андроид автоматически включается wifi

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

CommandBuffer

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

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

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

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

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

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

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

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

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

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

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’ом.

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

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

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

Источник

Оцените статью