Почти каждый растущий проект рано или поздно начинает стремиться к многомодульной архитектуре.
Разработчики не хотят ждать, пока весь проект будет полностью перестроен, когда была изменена только одна функция.
Мультимодульность помогает изолировать функции приложения друг от друга, тем самым сокращая время сборки.
Но такая изоляция накладывает некоторые ограничения на объем компонентов.
Когда мы используем навигацию Jetpack в проекте с одним модулем, граф навигации доступен из любого пакета приложения, мы всегда можем явно указать, какое действие должен выполнять NavController, и есть доступ к глобальному хосту, если в проекте есть вложенные фрагменты.
Но когда модулей много, возникают вопросы: где строить граф навигации, как к нему обращаться и как не запутаться в зависимостях модулей.
Обо всем этом мы поговорим под катом.
График навигации Самое важное, что следует помнить при проектировании многомодульного приложения, — это зависимости.
Зависимости в дереве зависимостей модуля должны быть направлены в одном направлении.
Наиболее зависимым модулем в многомодульном приложении всегда является модуль приложения.
Он знает почти обо всех остальных модулях.
Приложения обычно реализуют внедрение зависимостей с использованием различных платформ.
Используя эту зависимость модуля приложения, вы можете реализовать в нем граф навигации основного хоста.
Всегда следует помнить, что модуль приложения должен реализовывать как можно меньше функционала, поскольку он наиболее зависим и практически любое изменение в проекте приведет к пересборке модуля приложения.
Разговор стоит дешево.
Покажи мне код И сразу к реальному примеру.
Наш случай: точкой входа в приложение является заставка, которая определяет, к какому экрану перейти дальше: к основному функционалу или к авторизации.
С экрана авторизации есть переход только к основному функционалу.
Как обычно строим граф навигации – ничего сложного.
Доступ к навигации внутри модуля Когда приходит время сделать переход с одного экрана на экран в другом модуле, возникает вопрос – как? Ведь внутри функционального модуля нет доступа к графу навигации для получения идентификатора действия, которое должен выполнить NavController. Это решается путем реализации DI с использованием интерфейсов.
Вместо функционального модуля, зависящего от глобального графа навигации из модуля приложения, мы создадим интерфейс и назовем его SomethingNavCommandProvider, переменные которого являются навигационными командами.
SplashNavCommandProvider.kt
Сам интерфейс поставщика команд будет реализован в модуле приложения, а класс навигационной команды будет иметь те же поля, что и аргументы метода NavController.navigate. NavCommand.ktinterface SplashNavCommandProvider { val toAuth: NavCommand val toMain: NavCommand }
data class NavCommand(
val action: Int,
var args: Bundle? = null,
val navOptions: NavOptions? = null
)
Давайте посмотрим, как это выглядит на практике.
С заставки возможны 2 перехода: на экран авторизации и на главный экран функционала.
В модуле заставки создаем интерфейс: SplashNavCommandProvider.kt interface SplashNavCommandProvider {
val toAuth: NavCommand
val toMain: NavCommand
}
В модуле приложения мы создаем реализацию этого интерфейса и с помощью di framework (я использую Dagger) предоставляем ее модулю через интерфейс Splash. SplashNavCommandProviderImpl.kt — реализация CommandProvider class SplashNavCommandProviderImpl @Inject constructor() : SplashNavCommandProvider {
override val toAuth: NavCommand = NavCommand(R.id.action_splashFragment_to_authFragment)
override val toMain: NavCommand = NavCommand(R.id.action_splashFragment_to_mainFragment)
}
SplashNavigationModule.kt — модуль DI для обеспечения зависимости @Module
interface SplashNavigationModule {
@Binds
fun bindSplashNavigator(impl: SplashNavCommandProviderImpl): SplashNavCommandProvider
}
AppActivityModule.kt — основной модуль приложения DI @Module
interface AppActivityModule {
@FragmentScope
@ContributesAndroidInjector(
modules = [
SplashNavigationModule::class
]
)
fun splashFragmentInjector(): SplashFragment
…
}
В модуле сплеша реализуем реализацию в MV (здесь) это либо Presenter, либо ViewModel. SplashViewModel.kt class SplashViewModel @Inject constructor(
private val splashNavCommandProvider: SplashNavCommandProvider
) .
Когда логика экрана считает, что пришло время перейти на другой экран, мы передаем команду нашему фрагменту и приказываем ему перейти на другой экран.
Мы могли бы реализовать реализацию SplashNavCommandProvider непосредственно во фрагменте, но тогда мы потеряли бы возможность тестировать навигацию.
В самом фрагменте для выполнения перехода нужно получить NavController. Если текущий экран не является вложенным фрагментом, то мы просто получаем NavController с помощью метода findNavController() и вызываем его метод навигации: findNavController().
navigate(toMain)
Можно сделать немного удобнее, написав расширение для фрагмента ФрагментExt.kt fun Fragment.navigate(navCommand: NavCommand) {
findNavController().
navigate(navCommand.action, navCommand.args, navCommand.navOptions)
}
Почему только на фрагмент? Поскольку я использую подход SingleActivity, если у вас их несколько, то вы также можете создавать расширения для Activity. Тогда навигация внутри фрагмента будет выглядеть так navigate(toMain)
Вложенные фрагменты Навигация во вложенных фрагментах может быть двух типов:
- Навигация внутри вложенного контейнера
- Перемещает контейнер на один или несколько уровней выше.
Например, глобальная активность хоста
А для выполнения перехода во втором случае нужно получить NavController нужного хоста.
Для этого вам нужно получить id этого хоста внутри модуля.
Поскольку доступ к нему имеет только модуль, в котором реализован граф навигации этого хоста, мы создадим зависимость и реализуем ее в функциональных модулях, где необходим доступ к конкретному NavController через Dagger. GlobalHostModule.kt — модуль DI для обеспечения зависимости от глобального идентификатора хоста.
@Provides
@GlobalHost
fun provideGlobalHostId(): Int = R.id.host_global
AppActivityModule.kt — основной модуль приложения DI @FragmentScope
@ContributesAndroidInjector(
modules = [
GlobalHostModule::class,
ProfileNavigationModule::class,
.
]
)
fun profileKnownFragmentInjector(): ProfileKnownFragment
Внедрение зависимости идентификатора хоста во фрагмент @Inject
@GlobalHost
var hostId = 0
При наличии вложенности фрагментов стоит создать квалификатор для каждого хоста или использовать существующий квалификатор Named, чтобы Dagger понимал, какой int ему необходимо предоставить.
GlobalHost.kt @Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class GlobalHost
После того, как во фрагменте получен идентификатор зависимости нужного хоста, вы можете получить NavController по идентификатору хоста.
Давайте улучшим наше расширение, чтобы можно было делать переходы в любом контейнере: ФрагментExt.kt fun Fragment.navigate(navCommand: NavCommand, hostId: Int? = null) {
val navController = if (hostId == null) {
findNavController()
} else {
Navigation.findNavController(requireActivity(), hostId)
}
navController.navigate(navCommand.action, navCommand.args, navCommand.navOptions)
}
Код во фрагменте navigate(toAuth, hostId)
Это были основные моменты организации навигации с помощью Jetpack в многомодульной архитектуре.
Если у вас есть вопросы, буду рад ответить на них в комментариях :) Теги: #Android #Разработка Android #Kotlin #jetpack #компонент навигации
-
Apache Atlas — Доступный Каталог Данных
19 Oct, 24 -
Пространство И Холст
19 Oct, 24 -
Клон Asus Eeepc?
19 Oct, 24 -
Предупрежден – Значит Разумный Вызов
19 Oct, 24 -
Куда Двигаться С Помощью Parse Push?
19 Oct, 24 -
Загрузка Данных В Postgresql
19 Oct, 24