В этой статье вы узнаете, как можно организовать графы отдельных модулей / фич / user story, централизовать их, построить прямую навигацию между ними и присыпать сверху Safe Args плагином.
Вы сейчас в третьей части большого материала про Navigation Component в многомодульном проекте. Если не поняли ни единого слова выше, то призываю сначала ознакомиться с тем:
-
Что за зверь этот Navigation Component.
-
Как работает плагин Safe Args и что он делает.
Ну а если вы уже знакомы с этой библиотекой, то для вас есть приятный бонус в следующей статье подход к организации iOS-like multistack-навигации.
Сначала посмотрим, как выглядит разбиение проекта на модули у нас в компании, в которой я работаю (magora-systems.com):
-
:app основной модуль и точка входа в приложение. Он должен знать обо всех модулях, участвующих в приложении.
-
:core-модуль содержит в себе все базовые вещи: базовые классы, модели, entity, DTO, extension-ы и пр.
-
Утилитарные модули служат для инкапсуляции функционала основных компонентов приложения. Например, работа с сетью, БД или той же навигацией.
-
Feature-модули заключают в себе работу определенной фичи / user story, будь то флоу или экран.
Что ж, давайте натянем сову на глобус сделаем навигацию с
подключенным Safe Args плагином.
Если пользоваться одним единственным графом, то будет так:
Выглядит запутанно и неочевидно. Чтобы не допустить подобного, нужно совершить ряд действий:
-
Организовать графы для каждого feature-модуля.
-
Выделить отдельный Top-level граф.
-
Сделать удобные переходы между графами модулей.
-
Решить, где хранить Top-level граф.
Теперь подробнее о каждом.
Организовать графы для каждого feature-модуля
Чтобы не было проблем с навигацией между destination-ами внутри одного модуля, сделаем отдельные графы под каждый feature-модуль и там обозначим все конечные точки и связи между ними. При таком подходе навигация фичи полностью инкапсулируется внутри модуля, а Safe Args классы, принадлежащие соответствующему графу, генерируются только тут.
Выделить отдельный Top-level граф
Главный граф это тот, который является стартовой точкой навигации приложения и заключает в себе все необходимые графы, между которыми происходит навигация.
Выглядит не так эффектно, зато эффективно.
Сделать удобные переходы между графами модулей
А для того, чтобы можно было навигироваться между графами различных фич, нужно подвести к каждому global action.
Решить, где хранить Top-level граф
Тут есть несколько вариантов, у каждого есть свои плюсы и минусы:
-
Базовый модуль (:core)
О нем знает большинство модулей, но не все. Он не знает ни об одном модуле, поэтому не сможет увидеть графы. Стоит заметить, что приложение скомпилится и будет работать несмотря на ошибки Lint-a, при мерже ресурсов все равно всё сливается воедино.
При изменении графа надо будет пересобирать проект, а так как от него зависит много модулей, то их тоже надо будет пересобрать.
-
Главный модуль (:app)
+ Знает об абсолютно всех модулях.
О нем не знает ни один модуль.
Safe args сгенерирует global action-ы в недоступном для feature-модулей месте, поэтому мы не сможем ходить между графами.
Результаты не то чтобы очень радужные, но у нас есть целый один плюс в пользу :app-модуля, так что оставим Top-level граф в нем и займемся минусами этого подхода.Для этого нам понадобится
Минус: о нем не знает ни один модуль
Решение: сделать отдельный модуль (:navigation), о котором будут знать абсолютно все модули, которые будут хоть как-то взаимодействовать с навигацией.
Добавить в него все id глобальных action-ов. Таким образом generated-файлы поймут, с чем работают, и будут иметь доступ к id каждого глобального перехода.
<item name="actionglobalnavsignin" type="id"/><item name="actionglobalnavsignup" type="id"/><item name="actionglobalnavhome" type="id"/><item name="actionglobalnavuserslist" type="id"/><item name="actionglobalnavuserdetails" type="id"/><item name="actionglobalnavonC11CglobalC12Csettings" type="id"/><item name="actionC13Cto_faq" type="id"/>
Минус: сгенерированные Directions и Args лежат в :app модуле
Safe args сгенерирует global action-ы в недоступном для feature-модулей месте, поэтому мы не сможем ходить между графами.
Решение: перенести и доработать generated-файлы. Тут немного
сложнее и придется запачкать руки о билд-скрипты. Generated-классы
находятся в build-папке того модуля, где находится граф (сейчас это
:app), а использовать его в :navigation-модуле неудобно. Поэтому
воспользуемся костылем небольшой хитростью: во время билда
дождемся конца работы таски generateSafeArgs, перекинем все
созданные файлы в модуль навигации и, так как Args- и
Directions-классы используют R файл модуля :app, добавим импорт
нашего модуля навигации.
ext {navigationArgsPath = '/build/generated/source/navigation-args'appNavigation = "${project(':app).projectDir.path}$navigationArgsPath"navigationPath = "${project(':navigation').projectDir.path}$navigationArgsPath"navigationPackage = com.example.navigation}tasks.whenTaskAdded { task ->if (task.name.contains('generateSafeArgs')) {task.doLast {fileTree(appNavigation).filter { it.isFile() && it.name.contains("Directions") }.forEach { file ->if (file.exists()) {def lines = file.readLines()lines = lines.plus(2, "import $navigationPackage.R")file.text = lines.join("\n")}}}move(file("$appNavigation"), file("$navigationPath"))}}
В итоге
В результате мы получаем единый флоу работы, при соблюдении которого навигация в приложении декомпозируема, легко масштабируема и работает как часы. Даже если в проекте понадобится переиспользовать какую-нибудь часть графа, то её можно вынести в отдельный модуль, сделать отдельный граф и переиспользовать, как вам заблагорассудится.
Помимо всего прочего мы решили проблему с созданными плагином файлами теперь ребилд в разные флейворы и типы не будет нам докучать, как бы это было с простым добавлением sourceSet-s в модуль навигации.
На этой победе я останавливаться не стал и решил посмотреть, что еще можно с этим сделать. Для этого как никогда кстати подошел проект, в котором заказчик хотел приложение с нижним меню и чтобы каждая вкладка сохраняла свое состояние при уходе с неё. Именно о таком решении финальная часть моей истории про iOS-like multistack-навигацию.