Пример модульного андроид приложения с помощью Navigation component и Koin (DI)

от автора

Разработчик, привет!

В этом статье я хочу поделиться примером модульного андроид приложения с помощью NavComponent (JetPack) и Koin (DI).

У нас в компании есть много разных андроид проектов, которые должны использовать фичи (Feature) друг друга — это некая экосистема. Чтобы этого добиться нам необходимо разрабатывать эти фичи максимально независимыми и гибкими.

Требования к фичам можно сформулировать так:

  1. Фича должна иметь возможность подменить логику и UI независимо друг от друга.

  2. Для вызова фичи должно быть достаточно знать её интерфейс (метку) и необходимые параметры входа/выхода.

  3. Сама фича должна скрывать свою реализацию от других фич.

  4. Поддержка feature on-off из коробки.

Для меня feature — это логически объединенная система компонентов, которая выполняет некоторую функцию в приложении. Например, авторизация, покупка. Фича содержит свой UI граф (цепочку фрагментов), который должен храниться в backStack навигации, а после выхода из фичи, возвращать пользователя в точку вызова фичи.

Условная схема приложения из двух фич:

На схеме приложение, в котором есть MainFragment (M) и две фичи (А и В). Из М можно перейти в фичи А и В. А из фичи В мы можем попасть в фичу А. Причем, после завершения фичи А мы должны вернуться в точку вызова: если мы попали в фичу А из M, то возвращаемся в М, а если вызывали фичу А из В, то в В. При этом стек навигации должен сохраняться. А1, А2, B1, B2 — фрагменты фич.

Каждая фича состоит из трех физических модулей андроид проекта: API, BL, UI.

На рисунке стрелка указывает направление зависимости.  A -> B — модуль А зависит от В.

  • API модуль содержит интерфейсы и модели данных фичи, с которыми могут работать все другие модули. Lint правило: На API модуль могут ссылаться любые другие модули.

  • BL модуль содержит бизнес-логику фичи. Модуль реализует API фичи. Модуль содержит internal классы — закрыт для других модулей, кроме DI компонента, которые подключается в приложение. Lint правило: На BL модуль может  ссылаться только APP модуль.

  • UI модуль содержит интерфейс фичи. Модуль содержит internal классы — закрыт для других модулей, кроме DI компонента, которые подключается в приложение и NavGraph, который подключается в граф приложения как nested graph. Lint правило: На UI модуль может ссылаться только APP модуль.

А приложение строится следующим образом:

  • Мы создаем наши фичи модули

  • Создаем модуль приложения (app)

  • В app модуле собираем наш паззл компонентов из DI модулей

startKoin {             modules(                 listOf(                     AppKoinModule.create(),                                      FeatureAImplKoinModule.create(),                     FeatureAUiKoinModule.create(),                     FeatureBImplKoinModule.create(),                     FeatureBUiKoinModule.create()                 )             )         }

Чтобы изменить логику фичи, вам нужно реализовать дополнительный модуль, подключить к app и заменить всего лишь одну строчку в DI, и эта логика сразу распространится по всему приложению, не важно кто и где вызывает фичу. C UI модулем есть нюанс — это граф, его нужно будет добавить в root граф app модуля.

  • Добавляем графы наших фич в root граф нашего приложения (app). Пока только статическое формирование графа, работаю над динамической, там немало проблем.

  • Вызов фичи выглядит следующим образом:

 appNavigator.navigateTo(FeatureADestination::class.java)

Вот как выглядит интерфейс (по сути это просто метка)

interface FeatureADestination : ModuleNavInfo

А интерфейс ModuleNavInfo содержит уже более конкретную информацию, как фичу нужно вызывать. Реализация FeatureADestination спрятана в UI модуле.

interface ModuleNavInfo {     fun getNavigationStartPointResId(): Int      fun isFeatureAvailable(): Boolean }

Также в приложении есть navigator, который достает нужные фичи по их интерфейсам и выполняет переход. Реализация навигатора достаточно простая, вам нужно лишь вытащить зарегистрированную в DI реализацию ModuleNavInfo:

class KoinAppNavigator : AppNavigator {      private val navigationDestinationInternal = MutableLiveEvent<EventArgs<ModuleNavInfo>>()     override val navigationDestination =         navigationDestinationInternal as LiveData<EventArgs<ModuleNavInfo>>      private val navigationIntDestinationInternal = MutableLiveEvent<EventArgs<Int>>()     override val navigationResDestination: LiveData<EventArgs<Int>>         get() = navigationIntDestinationInternal      override fun <T : ModuleNavInfo> navigateTo(         moduleNavInfo: Class<T>     ) {         val destination = KoinJavaComponent.get(moduleNavInfo) as ModuleNavInfo         navigationDestinationInternal.postValue(EventArgs(destination))     }      override fun navigateTo(destination: Int) {         navigationIntDestinationInternal.postValue(EventArgs(destination))     }      override fun <T : ModuleNavInfo> resolveModule(moduleNavInfo: Class<T>): ModuleNavInfo? {         return try {             KoinJavaComponent.get(moduleNavInfo)         } catch (e: Exception) {             null         }     }      override fun <T : ModuleNavInfo> isCanNavigateTo(moduleNavInfo: Class<T>): Boolean {         return resolveModule(moduleNavInfo) != null     } } 

ModuleNavInfo и AppNavigator можно расширить, здесь я показал самый простой пример. Например, вам точно понадобится передавать параметры и возвращать результат. Сделать это тоже можно через ModuleNavInfo. Например, для возврата результата фичи, можно добавить StateFlow и подписываться на него. Все получается лаконично и в одном месте. Также можно настроить взаимодействие с помощью внутренних сервисов, которые можно спрятать от внешнего мира в BL модулях.

Код примера доступен на гитхаб.

Спасибо за внимание!

ссылка на оригинал статьи https://habr.com/ru/post/558494/


Комментарии

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

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