Реализация Lazy Dependency Holder (Продвинутая ленивая инициализация зависимостей) для больших команд

—

от автора

Эта статья для тех кто занимается оптимизацией сборки или просто хочет получить расширяемый шаблон для своего стартапа!

Роли озвучивали В проекте-шаблоне используются ComposeUI, Retrofit c Моками, Многомодульность, Api/Impl, SOLID, CLEAN, lazy dependency holder, Dagger2, Kotlin dsl, гибридная система навигации AnimatedNavHost/FragmentManager.

Качай да пользуйся!

Статья состоит из двух частей:

Часть 1. Обзор проекта, описание макро деталек

Часть 2. Собственно разбор этой вашей ленивой инициализации

Пару слов от автора: шаблон для для больших команд, так что не надо пытаться натянуть сову на глобус, а просто посмотри как это работает у больших и волосатых 🙂

Вдохновлялся статьей отсюда, все по той же теме, рекомендую к прочтению! Моё почтение автору.

Часть 1. Обзор проекта, описание макро деталек

Давайте немножко вспомним про CLEAN — вот шпаргалка.

рис.1 CLEAN ARCHITECTURE

рис.1 CLEAN ARCHITECTURE

Отсюда мы видим что:

  1. Строгое деление на модули(Кэп:))

  2. Модули строго ограничены по своему функционалу именно таким образом как на картинке — сильное расхождение уже не clean! так то!

  3. Наш проект — кононичный 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") } }      ...........                 

Тут три примера экранов:

  1. с пробросом роутера,

  2. пустой,

  3. и в который можно попасть кликнув на пуш

При этом мы сохраняем гибридный бекстек (win!), получаем хорошую скорость сборки, свободу от иногда (часто) тормозящих бекэндеров, и прекрасно децентрализованную систему в которой большое количество команд не будут мешать друг другу.

О, ну и конечно, такую сладкую и супер классную ленивую инициализацию зависимостей!
которая будет скармливать сборщику мусора неиспользуемые в данный момент модули.

P. S.

Ссылку на проект шаблон прилагаю — пользуйся, честной народ, на здоровье!

На момент написания статьи фреймворк Dagger2 стало возможным обновить с процессора KAPT до процессора KSP(alpha), а это значит что с новыми возможностями Dagger2 станет еще быстрее!(win) Пруф.

Так же имеет смысл обновить этот шаблон‑проект до новой версии gradle потому что там тоже очень много вкусного подвезли 🙂

Ну что ж, друзья, надеюсь, эта информация была вам полезна, обязательно пишите свое мнение!

До новых встреч 🙂


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


Комментарии

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

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