Разделяй и властвуй: Navigation Component в многомодульном проекте
В этой статье вы узнаете, как можно организовать графы отдельных модулей / фич / user story, централизовать их, построить прямую навигацию между ними и присыпать сверху Safe Args плагином.
Посмотреть подход в действии можно тут
Вы сейчас в третьей части большого материала про Navigation Component в многомодульном проекте. Если не поняли ни единого слова выше, то призываю сначала ознакомиться с тем:
Ну а если вы уже знакомы с этой библиотекой, то для вас есть приятный бонус в следующей статье — подход к организации iOS-like multistack-навигации.
Сначала посмотрим, как выглядит разбиение проекта на модули у нас в компании, в которой я работаю (magora-systems):
: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="action_global_nav_sign_in" type="id"/>
<item name="action_global_nav_sign_up" type="id"/>
<item name="action_global_nav_home" type="id"/>
<item name="action_global-nav_userslist" type="id"/>
<item name="action_global_nav_userdetails" type="id"/>
<item name="action_global_nav_on_settings" type="id"/>
<item name="action_to_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.metapoger.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-навигацию.