
Эта статья для тех кто занимается оптимизацией сборки или просто хочет получить расширяемый шаблон для своего стартапа!
Роли озвучивали В проекте-шаблоне используются ComposeUI, Retrofit c Моками, Многомодульность, Api/Impl, SOLID, CLEAN, lazy dependency holder, Dagger2, Kotlin dsl, гибридная система навигации AnimatedNavHost/FragmentManager.
Статья состоит из двух частей:
Часть 1. Обзор проекта, описание макро деталек
Часть 2. Собственно разбор этой вашей ленивой инициализации
Пару слов от автора: шаблон для для больших команд, так что не надо пытаться натянуть сову на глобус, а просто посмотри как это работает у больших и волосатых :)
Вдохновлялся статьей отсюда, все по той же теме, рекомендую к прочтению! Моё почтение автору.
Часть 1. Обзор проекта, описание макро деталек
Давайте немножко вспомним про CLEAN - вот шпаргалка.

Отсюда мы видим что:
Строгое деление на модули(Кэп:))
Модули строго ограничены по своему функционалу именно таким образом как на картинке — сильное расхождение уже не clean! так то!
Наш проект — кононичный clean! и у него даже есть несколько фича‑модулей.
в среднем каждый проект про который говорят что он сделан по clean должен выглядеть примерно так как на рис.1
И так с шаблоном архитектуры разобрались теперь немножко поговорим про навигацию. Тут используется Compose c библиотекой навигации основанной на классе ‑AnimatedNavHost.
Так как статья не про эту либо отмечу лишь одну важную вещь — AnimatedNavHost есть надстройка над NavHost и основной проблемой ее внедрения стала невозможность прямого доступа к backStackEntry, а этот доступ в свою очередь, нужен был для создания сложных сценариев таких как возврат назад с подменой destination (оно же backStackEntry).
Познакомиться со строением графа вы можете в файле ComposeRootFragment.
В общем, кто как решил эту проблем,у пишите в комментариях будет очень интересно!
В проекте так же использован гибридный подход к Compose т. е. существуют два навигационных графа, один из которых — на фрагментах, а второй это наш AnimatedNavHost. Подход к построению графа вы можете изучить самостоятельно однако подмечу очень важный нюанс — попытка сделать навигацию гибридной породила довольно сложную системы вложенных лямбд
где в файле Routing вы сможете найти такую конструкцию:
val content: ((String, NavOptionsBuilder.() -> Unit) -> Unit) -> @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit
Проще сделать к сожалению не получилось :-(
Так что если у вас есть решение смело выкладывайте в комментариях! :))
Навигаторы лежат в MainActivity как методы routeToCompose(..) и roteToFragment(..)
Пожалуйста, напишите кто и как решал такие вот примеры на своем опыте!
И если вы хотите чтобы я рассказал вам про реализацию этой навигации подробнее то обязательно напишите в комментарии об этом тоже!
Отдельно стоить отметить про сетевое взаимодействие — оно реализовано c использованием системы заглушек так, чтобы мы могли полностью отвязаться от сервера и вести разработку в своем таймлайне. Это чрезвычайно удобно!
Реализация лежит в файле MockConfig и все файлы в этой папке так же задействованы.
Собирает это все наш прекрасный Gradle под оберткой KotlinDSL. Про этот подход как мне кажется уже должны знать все и если не используют его то смело на него переходить!
а ну быстро давай переходи на KotlinDSL! :)
И если коротко то подход этот позволяет реализовать следующую конструкцию в gradle файлах зависимых модулей:
plugins { id("com.android.library") kotlin("android") kotlin("kapt") } android { compileSdk = compileSdkVersionConf } initLibDependencies()
Ну разве сие не прекрасно!?
plugins { id("com.android.library") kotlin("android") kotlin("kapt") } android { compileSdk = compileSdkVersionConf } initLibDependencies() dependencies { implementation(project(":common")) implementation(project(":features:bfeatureapi")) }
А так выглядит подключение модулей, предоставляющих зависимости в наш модуль!
естественно то все зависимости хитрым образом убраны по дальше и представляют из себя коллекции коллекций (мапы мап) объединены по принципу подключаемого фреймворка. Все это лежит в Config.kt обязательно ознакомься.
Самые внимательные заметят что тут используется подход impl/api
про него можно так же написать отдельную статью, однако я лишь помечу, что это нужно для для того, чтобы более удобно вести разработку в больших и пересекающихся командах. Раньше мы могли увидеть прирост производительности инкрементальной компиляции с помощью правильного построения архитектуры, используя как раз impl api. Но согласно последней информации — новый (на тот момент) Gradle прекрасно справляется сам. Пруф.
По моим тестам из прошлого я лишь могу сказать, что подход impl/api действительно сокращал время компиляции и довольно ощутимо, ну а без этого подхода я программы не пишу так что кто хочет может скинуть свои тесты в комментарии! Это было бы круто!
Часть 2 — Собственно разбор этой вашей ленивой инициализации
или
Реализация Lazy Dependency Holder (Ленивая инициализация зависимостей) в многомодульном проекте для больших команд
Итак, Котятки! Мы подошли к самому вкусному!
А именно как же была реализована наша прекрасная ленивая инициализация модулей,
которая позволит нам освобождать память, которую наше приложение так безмерно любит кушац.
Начнем с базовой конструкции которую вы можете посмотреть в файлах класса LazyController
open class LazyController<T> { private var lazyObject: WeakReference<Lazy<T>>? = null protected lateinit var setLazyInstanceFunction: () -> T protected var strongRefInstance: Lazy<T>? = null fun setLazyInstance(setLazyInstanceFunction: () -> T) { this.setLazyInstanceFunction = setLazyInstanceFunction } open fun getLazyInstance(): T { if (lazyObject == null || lazyObject?.get() == null) { strongRefInstance = lazy { setLazyInstanceFunction() } // Инициализация strongRef lazyObject = WeakReference(strongRefInstance) CoroutineScope(Job()).launch { delay(1000) // Задержка на 1 секунду //кейс маловероятный но возможно удаление при очень быcтрой работе gc //чтобы gc не собрал его сразу делаем сильную ссылку и очищаем ее через секунду strongRefInstance = null // Удаление strongRef } } return lazyObject!!.get()!!.value ?: throw IllegalArgumentException( "instance not yet initialized ( need to use method .setLazyInstance() first )" ) } }
Тут нужно обратить внимание на generic — WeakReference<Lazy>?
Именно он позволяет использовать механику делегата Lazy и отдавать всю работу по управлению памятью виртуальной машине java c помощью WeakReference.
Логика довольно проста: дай мне ссылку на холдер (мы используем подход Dependecy Holder) если она у тебя есть а если ее нет то создай новую. При этом возвращается именно слабая ссылка а сильная затирается, делая возможной сборку неиспользуемого модуля Сборщиком Мусора (GC).
Cтоит упомянуть, что на базе LazyController реализован класс LazyControllerSingleton который инициализируется всего 1 раз и нужен для инициализации модуля Network и других модулей уровня ядра.
Теперь давайте рассмотрим то где и как этот контроллер применяется
@Component( modules = [ComposeRootModule::class] ) @MyModuleScope interface ComposeRootComponent { fun inject(composeFragment: ComposeRootFragment) fun getFragmentPatches(): Map<String, (Bundle?) -> Fragment > @Component.Builder abstract class Builder { abstract fun build(): ComposeRootComponent @BindsInstance abstract fun insertRoutes(routerMap: Map<String, ComposablePatchData>): Builder } companion object { private var instance = LazyController<ComposeRootComponent>() fun getInstance() = instance.getLazyInstance() fun setInstance(setLazyInstanceFunction: () -> ComposeRootComponent) = instance.setLazyInstance { setLazyInstanceFunction() } } }
Перед вами уже элемент фреймворка Dagger2 а именно Component
( SubComponents я стараюсь не использовать по причине перерасхода ресурсов
вот тут один из умных мужей Яндекса рассказывает почему не надо юзать сабкомпоненты Пруф )
Для нашего понимания тут важны лишь 2 метода getInstance() и setInstance() где
getInstance используется для инициализации холдера а setInstance для подготовки холдера
инициализировать holder мы будем в классе DaggerComponentsInitializer
object DaggerComponentsInitializer { fun daggerComponentsInit(context: Context) { NetworkComponent.setInstance { DaggerNetworkComponent.builder() .insertAppContext(context) .build() } CFeatureComponent.setInstance { DaggerCFeatureComponent.builder().build() } AFeatureComponent.setInstance { DaggerAFeatureComponent.builder() .insertNetworkClient(NetworkComponent.getInstance().provideRetrofitClient()) .build() } ComposeRootComponent.setInstance { DaggerComposeRootComponent.builder().insertRoutes( //тут подключаются пути для новых композ дисплеев arrayListOf(CFeatureComponent.getInstance().fileExporters1()).getOneMap() ).build() } MainActivityComponent.setInstance { DaggerMainActivityComponent.builder() .insertRoutes( //тут подключаются пути для новых фрагментов arrayListOf( ComposeRootComponent.getInstance().getFragmentPatches(), AFeatureComponent.getInstance().getFragmentPatches() ).getOneMap() ).build() } } }
В этом инициализаторе происходит вот что: фактически мы описываем таблицу какие компоненты от каких компонентов зависят.
Получилось всё довольно интуитивно и понятно!
И теперь, чтобы все это заработало во фрагменте, нам достаточно просто написать такой код:
class AFeatureFragment : Fragment() { private val viewModel: AFeatureViewModel by lazyViewModel { stateHandle -> AFeatureComponent.getInstance().provideViewModel().create(stateHandle) } ..... }
я намеренно опускаю реализацию провайда вьюмоделей и работу роутинга так как это бы заняло материала еще на пару статей, отмечу однако что что с compose экранами все немножко по-другому:
инициализация compose экрана состоит из двух этапов
@Composable fun CFeatureMainComposeScreen( routeHandler: (String, NavOptionsBuilder.() -> Unit) -> Unit, viewModel: CFeatureViewModel ) {
В коде сверху мы видим собственно экран и уже прокинутую в него вью модель которая уже синхронизирована с жизненным циклом нашего экрана!
И так на первом этапе мы формируем нашу вьюмодель в модуле Dagger2 так:
object CFeatureModule { @Provides @IntoMap @StringKey(C_FEATURE_PATCH_NAME) fun getNavHostConfig1(): ComposablePatchData { return ComposablePatchData( C_FEATURE_PATCH_NAME, transitions = DownTransitions, content = { routeHandler -> { CFeatureMainComposeScreen(routeHandler, provideViewModelWithDependency { CFeatureComponent.getInstance().getViewModel() }) } } ) } ... }
Опять же функционирование роутинга в этой статье опускается и если будет интересно как все это работает то обязательно пишите в комментариях! Расскажу и про это тоже!
На текущий момент нужно понять что такая конструкция нужна для того чтобы дагер смог прокинуть зависимости вьюмодели и роутеры а так же и выдать это в хост(который AnimatedNavHost) по ключу C_FEATURE_PATCH_NAME и задача эта довольно нетривиальная учитывая гибридную природу навигации нашего шаблона.
Далее в строке номер 10 у нас происходит магия Dagger2 и мы получаем возможность лаконичного использования экранов compose с подвязанной вью моделью.
Все сложности и боли ради того, чтобы получить возможность так записывать граф!
И при этом команда могла вести разработку только в своем модуле и не тревожить остальных!
....... navController = rememberAnimatedNavController() AnimatedNavHost( navController = navController, startDestination = "homescreen", modifier = Modifier.weight(1f) ) { // инжект модулей с помошью Dagger @InToMap из Feature модуля routes.forEach { registerInNavHost(it.value, ::composeRouteHandler) } composable("orders") { OrdersScreen(::composeRouteHandler) } composable("homescreen") { HomeScreen() } composable( "details?{argument}", arguments = listOf(navArgument("argument") { type = NavType.StringType }), deepLinks = listOf(navDeepLink { uriPattern = "https://vvx.com?{argument}" }), ) { backStackEntry -> val article = backStackEntry.arguments?.getString("argument") OrdersScreen(::composeRouteHandler, "Showing $article") } } ...........
Тут три примера экранов:
с пробросом роутера,
пустой,
и в который можно попасть кликнув на пуш
При этом мы сохраняем гибридный бекстек (win!), получаем хорошую скорость сборки, свободу от иногда (часто) тормозящих бекэндеров, и прекрасно децентрализованную систему в которой большое количество команд не будут мешать друг другу.
О, ну и конечно, такую сладкую и супер классную ленивую инициализацию зависимостей!
которая будет скармливать сборщику мусора неиспользуемые в данный момент модули.
P. S.
Ссылку на проект шаблон прилагаю — пользуйся, честной народ, на здоровье!
На момент написания статьи фреймворк Dagger2 стало возможным обновить с процессора KAPT до процессора KSP(alpha), а это значит что с новыми возможностями Dagger2 станет еще быстрее!(win) Пруф.
Так же имеет смысл обновить этот шаблон‑проект до новой версии gradle потому что там тоже очень много вкусного подвезли :)
Ну что ж, друзья, надеюсь, эта информация была вам полезна, обязательно пишите свое мнение!
До новых встреч :-)
