Skip to content

Latest commit

 

History

History
299 lines (242 loc) · 26.4 KB

File metadata and controls

299 lines (242 loc) · 26.4 KB

Главная страница репозитория

Navigation

Предпосылки появления

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

Проблемы, которые решает модуль navigation.

  1. Нет единообразия при работе с навигацией для Activity, Fragment и Dialog. Запуск этих экранов выглядит совершенно по-разному и в android framework, и в предыдущих разработках Surf. Модуль navigation решает эту проблему и приводит всю работу с экранами к единой системе: навигации на основе команд.
  2. Сложность при организации Single-Activity приложений. В предыдущей реализации навигации для Fragments, у разработчиков была возможность опуститься только на один уровень вглубь стека фрагментов (можно было использовать FragmentNavigator для операций с supportFragmentManager, и ChildFragmentNavigator для операций с childFragmentManager). Операции с табами (TabFragmentNavigator) поддерживались только на уровне supportFragmentManager. Эта проблема решается в модуле navigation гибким запуском команд навигации: добавлена возможность углубиться на любой уровень вложенности фрагментов, и переключаться между этими уровнями.
  3. Асинхронность навигации. Все команды этого модуля выполняются последовательно и условно-синхронно, что позволяет вызывать цепочки команд не опасаясь за то, что предыдущий экран еще не будет готов.
  4. Излишнее количество реализаций Route. В старом подходе навигации у Route огромное количество реализаций, многие из которых просто не нужны, и заставляют пользователя путаться при выборе. В текущей реализации базовых Route есть всего несколько, и они разделены по типам экранов: ActivityRoute, FragmentRoute, Dialog и Widget.

Общий принцип работы.

Принцип работы модуля заключается в следующем:

  1. У каждого экрана есть точка входа: Route. Она содержит все входные данные, необходимые для запуска экрана, однозначно идентифицирует экран на основе этих параметров. Базовая реализация (BaseRoute) является базовым интерфейсом:
    • getScreenClass, getScreenClassPath - извлечение класса экрана, по которому будет происходит запуск экрана.
    • getTag - идентификация экрана
    • prepareData - предоставление стартовых данных для открытия экрана.
  2. Для каждого типа экрана существуют навигаторы (ActivityNavigator, FragmentNavigator, DialogNavigator), позволяющие выполнить открытие, закрытие и другие действия с экраном, основываясь на route. Навигаторы живут в скоупе экрана, умирают и пересоздаются вместе с ним. Если навигаторы содержат бекстек, он сохраняется и восстанавливается при смене конфигурации.
  3. Любое действие, производимое над экраном, заключается в класс NavigationCommand. Команда содержит в себе Route, возможные анимации Animations, и другие опции для открытия экрана.
  4. Для того, чтобы направлять команды в нужные навигаторы, существуют CommandExecutor-сущности. У каждого типа экрана есть свой Executor, и он ограничен пулом команд экрана, которые в него поступают. Все Executor'ы - это синглтоны, и живут столько же, сколько живет приложение.
  5. Все Executor'ы объединяются в один, общий AppCommandExecutor, который распределяет команды между экранными и обеспечивает последовательное выполнение.

Сохранение состояния и dependency injection

Данный модуль не привязан к стандартным скоупам core-ui, и не имеет зависимостей от DI-фреймворков. Сохранение состояния и привязка переменных к определенным экранам здесь реализована через ActivityLifecycleCallbacks и FragmentLifecycleCallbacks.

Все необходимые для Activity-навигации классы создаются в провайдерах: ActivityNavigationProvider, и его реализации ActivityNavigationProviderCallbacksaprovcal. Здесь для каждой Activity в onCreate создаются ActivityNavigator, DialogNavigator, а также меняется активный в данный момент навигатор в зависимости от того, какая Activity в данный момент находится в состоянии Resumed.

Все необходимые для Fragment-навигации классы создаются в провайдерах: FragmentNavigationProviderfprov, и его реализации FragmentNavigationProviderCallbacksfprovcal. Здесь для Fragment в onFragmentCreated создаются FragmentNavigator и TabFragmentNavigatortfnav, а также происходит переключение между навигаторами разных фрагментов: извлечение нужного происходит по идентификатору (тегу) фрагмента, в котором навигатор был создан.

Внимание! Fragment будет содержать навигаторы, только если он наследуется от FragmentNavigationContainer, то есть, в нем есть ViewGroup, внутри которого будут помещены дочерние фрагменты.

Извлечение нужного навигатора производится через FragmentNavigationProvider.provide(sourceTag). sourceTag здесь - это тег фрагмента, который содержит навигацию. В механизме навигации через CommandExecutor, этот тег предоставляется через FragmentCommand.sourceTag. Если необходимо явно вызвать навигатор из Activity (SupportFragmentManager), в качестве SourceTag может быть передан ACTIVITY_NAVIGATION_TAG.

Для того, чтобы постоянно не указывать sourceTag, существует ScreenScopeCommandExecutor, который действует на основе FragmentProvider, извлекает фрагмент из текущего Dagger-скоупа, и снабжает им команду.

Команды

Все команды NavigationCommand разделены по классам экранов:

  • ActivityNavigationCommand
    • Start - старт новой Activity
    • Finish - закрытие текущей открытой Activity.
    • FinishAffinity - закрытие текущего таска Activity.
    • Replace - замена текущей Activity на другую (Finish + Start).
  • FragmentNavigationCommand
    • Add - добавление фрагмента поверх текущего. Операция сохраняется в бекстек.
    • Replace - замена текущего фрагмента новым. Операция сохраняется в бекстек.
    • ReplaceHard - замена текущего фрагмента новым с одновременным закрытием текущего. Аналогична двум операциям: remove + add, и выполняется за одну транзакцию. Если у нас был стек из фрагментов A, B и мы выполняем ReplaceHard(C), то новым стеком будет A, C.
    • Remove - удаление фрагмента из контейнера. Операция невозвратная.
    • RemoveLast - удаление фрагмента, располагающегося на вершине бекстека, и показ предыдущего. Является обратной операцией к Add/Replace, и выполняет обратные анимации. Чтобы выполнить эту команду для TabFragmentNavigator, необходимо явно указать параметр isTab=true.
    • RemoveUntil - удаление фрагментов вплоть до указанного в команде.
    • RemoveAll - очистка бекстека. Возможна очистка всех фрагментов с сохранением последнего: необходимо явно указать параметр shouldRemoveLast=false. В случае с TabFragmentNavigator, команда RemoveAll очистит стек текущего активного таба.
  • DialogNavigationCommand
    • Show - показ диалога
    • Dismiss - скрытие диалога

Анимации

Для того, чтобы осуществить какую-либо команду с анимацией, необходимо передать в параметр Animations вашу анимацию, которую сможет обработать навигатор. Стандартные навигаторы поддерживают обработку BaseResourceAnimations, SharedElementAnimations и SetAnimationssetanim, служащий контейнером для первых двух.

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

Табы

Модуль поддерживает навигацию по табам (BottomNavigation). Для того, чтобы ваш экран поддерживал навигацию по табам и работал через TabFragmentNavigator, вам необходимо явно указать, что контейнер для навигации - это TabFragmentNavigationContainer. После этого, навигация в этом контейнере будет автоматически осуществляться через табы (TabFragmentNavigator).

Чтобы Route был вершиной таба (первым элементом, который находится в табе, и единственным, который сохраняется при очистке стека), необходимо унаследовать его от TabHeadRoute.

Механизм навигации по табам работает следующим образом:

TODO: Расписать подробнее механизм навигации по табам.

Подключение

Gradle:

    implementation "ru.surfstudio.android:navigation:X.X.X"

Инициализация компонентов

Для того, чтобы команды навигации выполнялись и обрабатывались, вам нужно инициализировать CommandExecutor в Application-классе вашего приложения:

  1. Создайте ActivityNavigationProvider. Вы можете использовать свою реализацию, либо готовый ActivityNavigationProviderCallbacks. Во втором случае, не забудьте зарегистрировать коллбеки в Application.

  2. Если вы используете модуль navigation-observer, инициализируйте ScreenResultBus. Для него потребуется хранилище ScreenResultStorage, можете использовать FileScreenResultStorage с директорией noBackupFilesDir.

  3. Создайте AppCommandExecutor: если вы используете модуль navigation-observer, это будет AppCommandExecutorWithResult(screenResultBus, activityNavigationProvider), иначе - AppCommandExecutor(activityNavigationProvider).

  4. Полученный CommandExecutor вы можете поместить в статическую переменную (если не используете DI), либо предоставить как singletone-зависимость в вашем DI-фреймворке.

Для примера инициализации без какого-либо DI-фреймворка, вы можете посмотреть на реализацию класса App из Navigation sample.

Для примера инициализации с использованием Dagger 2, вы можете посмотреть на реализаци класса App из Navigation surf sample.

Использование

Открытие экрана (Activity)

Для открытия Activity activityA нам потребуется:

  1. Создать RouteA, которая будет содержать все параметры, необходимые для инициализации и идентификации экрана. Унаследовать эту Route от ActivityRoute.
    • Переопределить getScreenClass, если route лежит в одном модуле с экраном
    • Переопределить getScreenClassPath, если route лежит в другом модуле, и не имеет прямого доступа к экрану
  2. Вызвать метод NavigationCommandExecutor.execute и передать в него команду Start(routeA).

Открытие экрана (Dialog)

Осуществляется аналогично открытию Activity, только route наследуется от DialogRoute, и вместо команды Start используется команда Showshowcom.

Открытие экрана (Fragment)

Для открытия fragmentA нам потребуется:

  1. Создать RouteA, которая будет содержать все параметры, необходимые для инициализации и идентификации экрана. Унаследовать эту Route от FragmentRoute.
    • Переопределить getScreenClass, если route лежит в одном модуле с экраном
    • Переопределить getScreenClassPath, если route лежит в другом модуле, и не имеет прямого доступа к экрану
  2. Унаследовать класс-родитель, содержащий ViewGroup-контейнер для отображения фрагмента, от интерфейса FragmentNavigationContainer и задать полю containerId значение с id этой ViewGroup.
  3. Извлечь тег экрана, открывающего этот Fragment (route.getTag / fragment.getTag).
    • Если экран открывает activity, этот шаг можно опустить.
    • Если в качестве NavigationCommandExecutor вы используете ScreenScopeCommandExecutor, этот шаг можно опустить.
  4. Вызвать метод NavigationCommandExecutor.execute и передать в него команду Add(routeA, sourceTag), где routeA - route, созданный на первом шаге, sourceTag - тег экрана, созданный на 3 шаге.

Цепочное выполнение команд

AppCommandExecutor поддерживает выполнение цепочек, состоящих как из синхронных, так и асинхронных команд. В зависимости от того какие команды есть в цепочке логика работы AppCommandExecutor будет немного отличаться. Но основной принцип - выполняется асинхронная команда, выполнение команд идущих после нее откладывается до момента, когда будет доступен новый ActivityNavigationHolder. И как только холдер станет доступен - срабатывает та же логика. Исключения из этого: * Выполнение цепочки команд Start, открывающее несколько активити сразу. * Выполнение асинхронных команд Replace и Start для активити после выполнения команд Finish и FinishAffinity. Replace и Start выполняются сразу после команд Finish и FinishAffinity в том же ActivityNavigationHolder, так как после выполнения команд закрывающих активити или стек активити в стеке может не остаться активити и последующие команды не выполнятся.

  1. Можно запускать несколько активити за один раз, передавая список из команд Start, например listOf(Start(ActivityRoute1()), Start(ActivityRoute2()), Start(ActivityRoute3())). У ActivityNavigator для выполнения этой цепочки команд будет вызван метод Context.startActivities.
  2. Можно асинхронно запустить активити-контейнер фрагментов и сразу добавить в него несколько фрагментов, AppCommandExecutor отложит выполнение всех команд, идущих после команды старта активити, дождется пока активити запутится и последовательно выполнит все синхронные команды, идущие после команды запуска активити.
  3. Можно закрыть несколько активити (без использования FinishAffinity). Для этого нужно передать список комманд Finish. Первая команда Finish из этого списка выполнится для текущей активити, последующие команды будут выполняться по мере того как будут становиться доступны холдеры ActivityNavigationHolder ранее запущенных активити.
  4. Можно выполнять запуск нескольких активити с добавлением в них фрагментов. listOf( Start(FirstActivityRoute()), Replace(FirstFragmentRoute()), Replace(SecondFragmentRoute()), Start(SecondActivityRoute()), Start(ThirdActivityRoute()), Replace(FirstFragmentRoute()), Replace(SecondFragmentRoute()) )
  5. Закрываем несколько активити из стека с последующим открытием новых активити. В этом случае нужно быть очень внимательным, так как если в стеке всего одна активити, а переданы две команды Finish и после них есть еще какие-то команды - выполнится только первая команда, которая закроет активити и вместе с этим все приложение и все последующие команды не будут выполнены. listOf( Finish(), Finish(), Start(SomeActivityRoute()) )