Pull to refresh

Разделяй и властвуй: Navigation Component в многомодульном проекте

Reading time 5 min
Views 6.5K

В этой статье вы узнаете, как можно организовать графы отдельных модулей / фич / user story, централизовать их, построить прямую навигацию между ними и присыпать сверху Safe Args плагином.

Посмотреть подход в действии можно тут

Вы сейчас в третьей части большого материала про Navigation Component в многомодульном проекте. Если не поняли ни единого слова выше, то призываю сначала ознакомиться с тем:

Ну а если вы уже знакомы с этой библиотекой, то для вас есть приятный бонус в следующей статье — подход к организации iOS-like multistack-навигации.

Сначала посмотрим, как выглядит разбиение проекта на модули у нас в компании, в которой я работаю (magora-systems):

  1. :app — основной модуль и точка входа в приложение. Он должен знать обо всех модулях, участвующих в приложении.

  2. :core-модуль содержит в себе все базовые вещи: базовые классы, модели, entity, DTO, extension-ы и пр. 

  3. Утилитарные модули служат для инкапсуляции функционала основных компонентов приложения. Например, работа с сетью, БД или той же навигацией.

  4. Feature-модули заключают в себе работу определенной фичи / user story, будь то флоу или экран.

Что ж, давайте натянем сову на глобус сделаем навигацию с подключенным Safe Args плагином.

Если пользоваться одним единственным графом, то будет так:

Выглядит запутанно и неочевидно. Чтобы не допустить подобного, нужно совершить ряд действий:

  1. Организовать графы для каждого feature-модуля.

  2. Выделить отдельный Top-level граф.

  3. Сделать удобные переходы между графами модулей.

  4. Решить, где хранить Top-level граф.

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

Организовать графы для каждого feature-модуля

Чтобы не было проблем с навигацией между destination-ами внутри одного модуля, сделаем отдельные графы под каждый feature-модуль и там обозначим все конечные точки и связи между ними. При таком подходе навигация фичи полностью инкапсулируется внутри модуля, а Safe Args классы, принадлежащие соответствующему графу, генерируются только тут.

Выделить отдельный Top-level граф

Главный граф — это тот, который является стартовой точкой навигации приложения и заключает в себе все необходимые графы, между которыми происходит навигация.

Выглядит не так эффектно, зато эффективно.

Сделать удобные переходы между графами модулей

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

Решить, где хранить Top-level граф 

Тут есть несколько вариантов, у каждого есть свои плюсы и минусы:

  1. Базовый модуль (:core)

О нем знает большинство модулей, но не все. Он не знает ни об одном модуле, поэтому не сможет увидеть графы. Стоит заметить, что приложение скомпилится и будет работать несмотря на ошибки Lint-a, при мерже ресурсов все равно всё сливается воедино.

При изменении графа надо будет пересобирать проект, а так как от него зависит много модулей, то их тоже надо будет пересобрать.

  1. Главный модуль (: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-навигацию.

Tags:
Hubs:
+8
Comments 4
Comments Comments 4

Articles