Хабрахабр

Летаем по модулям: Навигация в многомодульном приложении с Jetpack

Разработчики не хотят ждать пока пересобирается полностью весь проект, когда была изменена только одна фича. Почти каждый растущий проект рано или поздно начинает смотреть в сторону многомодульной архитектуры. Но такое изолирование накладывает некоторые ограничения на область видимости компонентов. Многомодульность помогает изолировать фичи приложения друг от друга, тем самым сокращая время сборки. Но когда модулей становится много, то возникают вопросы: где строить граф навигации, как получать к нему доступ и как не запутаться в зависимостях модулей. Когда мы используем навигацию из Jetpack в проекте с одним модулем, граф навигации доступен из любого пакета приложения, мы всегда можем явно указать какой action NavController должен выполнить, а также есть доступ к глобальному хосту, если в проекте есть вложенные фрагменты. Обо всем этом поговорим под катом.

Зависимости в дереве зависимостей модулей должны быть быть направлены в одну сторону. Самое важное о чем надо помнить при проектировании многомодульного приложения — зависимости.

Он знает почти о всех остальных модулях. Самым зависимым модулем в многомодульном приложении всегда является модуль app. Пользуясь такой зависимостью app модуля, в нем можно реализовать граф навигации основного хоста. В app обычно реализовывают DI с помощью различных фреймворков.

Всегда нужно помнить, что app модуль должен реализовывать как можно меньше функционала, так как он является самым зависимым и почти любое изменение в проекте приведёт к пересборке app модуля.

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

Когда приходит время делать переход с одного экрана на экран в другом модуле, возникает вопрос — как?

Ведь внутри модуля фичи нет доступа к графу навигации для получения action id, которое должен выполнить NavController.

Вместо того, чтобы модуль фичи зависел от глобального графа навигации из app модуля — мы создадим интерфейс и назовём его ЧтоТоNavCommandProvider, переменные которого — команды навигации. Это решается путем внедрения DI с помощью интерфейсов.

SplashNavCommandProvider.kt

interface SplashNavCommandProvider { val toAuth: NavCommand val toMain: NavCommand
}

Сам интерфейс провайдера команд будет реализовываться в app модуле, а класс команды навигации будет иметь те же поля, что и аргументы для метода NavController.navigate

NavCommand.kt

data class NavCommand( val action: Int, var args: Bundle? = null, val navOptions: NavOptions? = null
)

С экрана splash возможно 2 перехода: к экрану авторизации и к экрану основного функционала. Посмотрим как это выглядит на практике. В модуле splash создаем интерфейс:

SplashNavCommandProvider.kt

interface SplashNavCommandProvider { val toAuth: NavCommand val toMain: NavCommand
}

В модуле app создаем реализацию этого интерфейса и с помощью di фреймворка (у меня это 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

}

В splash модуль реализацию внедряем в MV(сюда) это либо Presenter, либо ViewModel…

SplashViewModel.kt

class SplashViewModel @Inject constructor( private val splashNavCommandProvider: SplashNavCommandProvider
) ...

Когда логика экрана считает, что пора переходить к другому экрану — мы передаём нашему фрагменту команду и сообщаем что надо выполнить переход на другой экран.

Можно было бы внедрять реализацию SplashNavCommandProvider прямо во фрагмент, но тогда мы лишаемся возможности тестировать навигацию.

Если текущий экран не вложенный фрагмент, то просто получаем NavController методом findNavController() и вызываем у него метод navigate: В самом фрагменте для выполнения перехода надо получить NavController.

findNavController().navigate(toMain)

Можно сделать немного удобнее, написав экстеншен для фрагмента

FragmentExt.kt

fun Fragment.navigate(navCommand: NavCommand) { findNavController().navigate(navCommand.action, navCommand.args, navCommand.navOptions)
}

Потому что я использую подход SingleActivity, если у вас их несколько, то можно создать экстеншены еще и для Activity. Почему только для фрагмента?

Тогда навигация внутри фрагмента будет выглядеть так

navigate(toMain)

Навигация во вложенных фрагментах может быть двух видов:

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

А для выполнения перехода во втором случае необходимо получить NavController нужного хоста. В первом случае все просто, нам подойдёт экстеншен который мы написали выше. Так как к нему есть доступ только у модуля, в котором реализован граф навигации этого хоста, то создадим зависимость и внедрим её в модули фич, где нужен доступ к конкретному NavController, через Dagger. Для этого внутри модуля надо получить id этого хоста.

GlobalHostModule.kt — DI модуль для предоставления зависимости id глобального хоста

@Provides
@GlobalHost
fun provideGlobalHostId(): Int = R.id.host_global

AppActivityModule.kt — основной DI модуль приложения

@FragmentScope
@ContributesAndroidInjector( modules = [ GlobalHostModule::class, ProfileNavigationModule::class, ... ]
)
fun profileKnownFragmentInjector(): ProfileKnownFragment

Внедрение зависимости id хоста во фрагмент

@Inject
@GlobalHost
var hostId = 0

Когда есть вложенность фрагментов, то стоит создавать по Qualifier у для каждого хоста или использовать существующий Qualifier Named, чтобы Dagger понимал какой именно int надо предоставить.

GlobalHost.kt

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class GlobalHost

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

FragmentExt.kt

fun Fragment.navigate(navCommand: NavCommand, hostId: Int? = null) else { Navigation.findNavController(requireActivity(), hostId) } navController.navigate(navCommand.action, navCommand.args, navCommand.navOptions)
}

Код во фрагменте

navigate(toAuth, hostId)

Если остались вопросы — то я с радостью отвечу на них в комментариях 🙂 Это были основные моменты организации навигации, используя Jetpack, в многомодульной архитектуре.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»