В данной статье вы ознакомитесь с довольно простой навигацией для Android.
В статье рассказывается про применение библиотеки в многомодульном проекте. Если вы хотите узнать как работает навигация этой библиотеки, то подробнее можно узнать в репозитории.
Немного про UDF
Для понимания как работает библиотека ознакомимся с UDF.
UDF состоит из следующих частей
State- источник правды или текущее состояние приложения в определенный момент времениView- UI отрисованный на основеStateAction- события в приложении, которые меняютState

Получается UDF, это когда Action меняет State, а View меняется на основе State.
Более подробно про UDF можно прочитать здесь.
Знакомство с библиотекой
Modo - это библиотека навигации для Android, которая основана на принципах UDF.
В данной библиотеке список экранов хранится как List в State
data class StackState(
val stack: List<Screen> = emptyList(),
)И есть экшены:
class Forward(val screen: Screen, vararg val screens: Screen) : NavigationAction
class Replace(val screen: Screen, vararg val screens: Screen) : NavigationAction
class NewStack(val screen: Screen, vararg val screens: Screen) : NavigationAction
class BackTo(val screen: Screen) : NavigationAction
...На это моменте можно уже понять раз библиотека основана на UDF, то Action меняет State, а State меняет состояние экранов.
Создаем навигацию
Часто в многомодульных проектах нужно получить определенную навигацию. Например, получить либо корнев��ю навигацию, либо получить навигацию фичи.
Поэтому для упрощения получения зависимостей навигации сначала продублируем экшены Modo
interface NavigationAction
data class NavigationForward(val screen: Screen, val screens: List<Screen> = emptyList()) :
NavigationAction
data class NavigationReplace(val screen: Screen, val screens: List<Screen> = emptyList()) :
NavigationAction
...Затем создадим маппер, чтобы переводить наши экшены в экшены Modo.
fun NavigationContainer<StackState>.navigate(command: NavigationCommand) {
val action = when (command) {
is NavigationSetStack -> SetStack(StackState(stack = command.screens))
is NavigationForward -> Forward(command.screen, *command.screens.toTypedArray())
...После создадим класс навигации, где мы отправляем наши экшены с помощью метода navigate(command: NavigationCommand) и слушаем наши экшены с помощью commandsFlow.
class Navigation {
private val _commandsFlow = Channel<NavigationCommand>()
val commandsFlow: Flow<NavigationCommand> = _commandsFlow.receiveAsFlow()
suspend fun navigate(command: NavigationCommand) {
_commandsFlow.send(command)
}
}
На этом приготовления для работы с навигацией заканчиваются.
Синхронизация навигации с экраном
Теперь начнем работать с библиотекой Dagger.
Создадим в даггер-модуле нашу навигацию.
@Module
internal class FeatureModule {
@Provides
@Singleton
@FeatureNavigationQualifier
fun provideNavigation(): Navigation = Navigation()
}Создаем интерфейс и прикрепим его к даггер-компоненте, чтобы получить доступ к нашей навигации.
interface FeatureDependenciesProvider {
@FeatureNavigationQualifier
fun navigation(): Navigation
}
internal interface FeatureComponent : FeatureDependenciesProviderЗатем создадим CompositionLocal с помощью которого мы будем получать доступ к навигации через интерфейс
val LocalFeatureDependenciesProvider = compositionLocalOf<FeatureDependenciesProvider> {
error("FeatureDependenciesProvider not found")
}Добавим навигацию в ViewModel и создадим слушатель экшенов navigationCommands, где в UI эти экшены уже будут мапиться в Modo-экшены
internal class FeatureViewModel @Inject constructor(
@FeatureNavigationQualifier private val navigation: Navigation,
) : ViewModel() {
val navigationCommands: Flow<NavigationCommand> = rootNavigation.commandsFlow
}
В UI будем использовать класс StackScreen из Modo, который рендерит последний экран из stack с помощью метода TopScreenContent()
@Parcelize
class FeatureStackScreen(
private val navigationModel: StackNavModel,
) : StackScreen(navigationModel = navigationModel) {
@Composable
override fun Content() {
val componentHolder = daggerViewModel {
ComponentHolder(DaggerFeatureComponent.builder().build())
}
val viewModel = daggerViewModel { componentHolder.component.viewModel() }
LaunchedEffect(Unit) {
viewModel.navigationCommands.collectLatest { command ->
navigate(command)
}
}
CompositionLocalProvider(
LocalFeatureDependenciesProvider provides componentHolder.component as FeatureDependenciesProvider
) {
TopScreenContent()
}
}
}
где, этот кусок кода отвечает за маппинг нашего экшена в modo экшен
LaunchedEffect(Unit) {
viewModel.navigationCommands.collectLatest { command ->
navigate(command)
}
}а этот кусок кода отвечает за рендеринг последнего экрана из stack c помощью метода TopScreenContent() и обеспечения зависимостями даггер-компоненты c помощью FeatureDependenciesProvider
CompositionLocalProvider(
LocalFeatureDependenciesProvider provides componentHolder.component as FeatureDependenciesProvider
) {
TopScreenContent()
}Осталось применить вышеперечисленный код для рутовой и фича навигации. Отличий в внедрении для рутового и фича навигации нет. Разве что нужно будет писать разные провайдеры зависимостей и разные Qualifier для каждой из навигации.
Используем в действии
Написание кода для разных навигаций не будет иметь никаких отличий. Даже root и feature навигации будут работать одинаково. Этот код всегда будет выглядеть так.
internal class FeatureViewModel @Inject constructor(
@FeatureNavigationQualifier private val navigation: Navigation,
private val screens: Screens,
) : ViewModel() {
init {
viewModelScope.launch {
rootNavigation.navigate(NavigationReplace(screens.someScreen()))
}
}
val navigationCommands: Flow<NavigationCommand> = navigation.commandsFlow
fun onSomeActionHappened() {
navigation.navigate(NavigationReplace(screens.someScreen()))
}
}
Представим более сложный проект, который состоит из двух основных фичей Complex и Simple, где фича Complex содержит внутри себя sub-feature. А Фича Simple представляет из себя простой экран.
Где будут следующие навигации:
Root Navigation - навигация основных фичей
Feature Navigation - навигация фичей Complex

Если в таком проекте нужно из фичи Complex открыть фичу Simple, то в нашем ViewModel будет два класса навигации:
Root Navigation - навигация, которая находится на уровень выше нашего экрана.
Feature Navigation - навигация фичи
Complex
Получается, когда отправится экшен открытия фичи Simple, то будет работать рутовая навигация, а не фичовая навигация.
internal class ComplexViewModel @Inject constructor(
@RootNavigationQualifier private val rootNavigation: Navigation,
@FeatureNavigationQualifier private val complex: Navigation,
private val rootScreens: RootScreens,
) : ViewModel() {
val navigationCommands: Flow<NavigationCommand> = featureNavigation.commandsFlow
fun onSomeActionHappened() {
rootNavigation.navigate(NavigationReplace(rootScreens.simpleScreen()))
}
}Заключение

В данной статье мы рассмотрели один из вариантов внедрения библиотеки Modo в многомодульном проекте. Можно дальше продолжать изменять описанную навигацию снижая зависимость от библиотеки или можно упростить работая напрямую с Modo, не создавая свои классы. Также вы можете более подробно ознакомиться с примером рассмотренным в статье.