
Использование библиотеки DI с новых взглядом набирает обороты. И автор хотел бы рассказать еще об одной идее, которую сподвигли сделать библиотекой такой, какой она сейчас является.
Собственные решения для разработчика развивать и разрабатывать оказалось крайне удобно. Архитектура библиотеки и ее фичи разрабатывались под конкретные задачи в проектах. И особенный случай в проекте, заставил переосмыслить всем привычные квалификаторы для DI, и добавить что-то новое.
Случай, кстати, оказался не новый, и автор много его встречал в различных проектах. Так что, думаю для многих из читателей такой пример покажется до боли знакомым.
Зазеркалье
Добро пожаловать на псевдо проект псевдо синего, зеленного или красного гиганта, который в штате содержит десятки человек на платформу. Несколько команд усердно пилят фичи. Ну а вы, как самый матерый разработчик, за всеми следите и планируете общую архитектуру проекта. Вы прослеживали рост проекта от самых зачатков до сегодняшних дней. И проект получился неплохим.
Не буду томить, ваше условное приложение является агрегатором такси. С огромной клиентской базой вы преуспеваете от ближайших конкурентов подробной аналитикой поездки, включая сегодняшнее настроение вызванного водителя и состояние его авто между тех. осмотрами. Каждый вызов такси сопровождается уникальным экраном ожидания со своими бонусами и пасхалками. Все это увеличивает вовлеченность клиента к приложению, а водителю понимание, что его не бросят посреди дороги.
И вот руководству понадобилось, что то новое. То, что обеспечит преимущество над всеми конкурентами разом - одновременный заказ 2х машин. Ну что мой опытный лид команды, боюсь даже представить, что будет с проектом, если ко всем условиям добавят сроки - один месяц. Ну а для подробности рассмотрим схему проекта.

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

Теперь наши компоненты: репозитории и датасорс, гвоздями прибиты к жизненному циклу приложения. Для разработки удобно, что они доступны для любого экрана. Но вот для масштабирования дела обстоят иначе. Синглтон просто так не задублируешь. Теперь их надо различать между собой в DI.
Поваренная книга
И вот мы планомерно пришли к знакомству с инструментарием Stone библиотеки - квалификаторами. Думаю вы уже с ними знакомы и из других библиотек DI, но все же уточню правило их использования.
@Qualifier @Retention(RetentionPolicy.RUNTIME) @Documented annotation class MainTripQualifier
Тут ничего нового вы не заметите. Даже применение этого самого квалификатора выглядит также, как и у известных фреймворков. Указывается квалификатор для метода провайдинга.
И тот же квалификатор должен быть для аргумента зависимости или поля инжекта.
@MainTripQualifier @Provide(cache = Provide.CacheType.Strong) abstract fun provideTripInfoRepositoryMain( api: TripInfoApi, @MainTripQualifier cache: TripInfoInMemory, ): TripInfoRepository
Деление же нашего приложение теперь сводится к простому объявлению дополнительных квалификаторов. Теперь мы можем разделить заказ такси на основной и дополнительный. Все компоненты, аналитика взаимодействие разделяются на 2 отдельных заказа.

Мы оставили только репозитории доступными как сингтоны, чтобы не растаскивать квалификаторы на всех. Но даже тут нам пришлось по всем слоям создавать отдельные методы предоставления под каждый квалификатор и объект. В особенности получилась страшная картина в модуле провайдинга интеракторов.
@Module abstract class InteractorsModule { @MainTripQualifier abstract fun provideTripInfoInteractorMain( @MainTripQualifier ordersRepository: CurrentOrderRepository, @MainTripQualifier tripInfoRepository: TripInfoRepository, ): TripInfoInteractor @SecondTripQualifier abstract fun provideTripInfoInteractorSecond( @SecondTripQualifier ordersRepository: CurrentOrderRepository, @SecondTripQualifier tripInfoRepository: TripInfoRepository, ): TripInfoInteractor @MainTripQualifier abstract fun provideMapItemsInteractorMain( @MainTripQualifier ordersRepository: CurrentOrderRepository, @MainTripQualifier tripInfoRepository: TripInfoRepository, ): MapItemsInteractor @SecondTripQualifier abstract fun provideMapItemsInteractorSecond( @SecondTripQualifier ordersRepository: CurrentOrderRepository, @SecondTripQualifier tripInfoRepository: TripInfoRepository, ): MapItemsInteractor }
Что-ж, с таким решением не один месяц можно прожить, но что дальше, ведь такое решение совсем не масштабируемое. Для нескольких заказов не нагенерируешь таких квалификаторов в проекте. Что если вообще этих заказов может быть неограниченное кол-во.
Hidden text
В конечном итоге, при инжекте разбирает все квалификаторы в цепочке зависимостей и инжектит в одном методе.
@Override public void inject(MapScreen mapScreen) { mapScreen.setViewModelMain(new ProvideBuilder<MapViewModel>((_lc0) -> { TripInfoInMemory _lc1 = data().provideTripInfoInMemoryMain(); TripInfoApi _lc3 = data().provideTripInfoApi(); CurrentOrderInMemory _lc5 = data().provideCurrentOrderInMemoryMain(); CurrentOrderApi _lc7 = data().provideCurrentOrderApi(); TripInfoRepository _lc9 = repository().provideTripInfoRepositoryMain(_lc3, _lc1); Ref<List<TripInfoRepository>> _lc10 = () -> new ProvideBuilder<TripInfoRepository>((_lc21) -> { _lc21.add(repository().provideTripInfoRepositoryMain(_lc3, _lc1)); }).all(); CurrentOrderRepository _lc11 = repository().provideCurrentOrderRepositoryMain(_lc7, _lc5); Ref<List<CurrentOrderRepository>> _lc12 = () -> new ProvideBuilder<CurrentOrderRepository>((_lc22) -> { _lc22.add(repository().provideCurrentOrderRepositoryMain(_lc7, _lc5)); }).all(); Ref<List<MapItemsInteractor>> _lc14 = () -> new ProvideBuilder<MapItemsInteractor>((_lc23) -> { _lc23.add(intractors().provideMapItemsInteractorMain(_lc11, ListUtils.first(NullGet.let(_lc10, Ref::get)))); }).all(); MapViewModel _lc15 = viewmodels().provideMapViewModelMain(ListUtils.first(NullGet.let(_lc14, Ref::get))); _lc0.add(_lc15); }).first()); mapScreen.setViewModelSecond(new ProvideBuilder<MapViewModel>((_lc25) -> { Ref<List<TripInfoInMemory>> _lc27 = () -> new ProvideBuilder<TripInfoInMemory>((_lc42) -> { _lc42.add(data().provideTripInfoInMemorySecond()); }).all(); TripInfoApi _lc28 = data().provideTripInfoApi(); Ref<List<CurrentOrderInMemory>> _lc31 = () -> new ProvideBuilder<CurrentOrderInMemory>((_lc44) -> { _lc44.add(data().provideCurrentOrderInMemorySecond()); }).all(); CurrentOrderApi _lc32 = data().provideCurrentOrderApi(); TripInfoRepository _lc34 = repository().provideTripInfoRepositorySecond(_lc28, ListUtils.first(NullGet.let(_lc27, Ref::get))); Ref<List<TripInfoRepository>> _lc35 = () -> new ProvideBuilder<TripInfoRepository>((_lc46) -> { _lc46.add(repository().provideTripInfoRepositorySecond(_lc28, ListUtils.first(NullGet.let(_lc27, Ref::get)))); }).all(); CurrentOrderRepository _lc36 = repository().provideCurrentOrderRepositorySecond(_lc32, ListUtils.first(NullGet.let(_lc31, Ref::get))); Ref<List<CurrentOrderRepository>> _lc37 = () -> new ProvideBuilder<CurrentOrderRepository>((_lc47) -> { _lc47.add(repository().provideCurrentOrderRepositorySecond(_lc32, ListUtils.first(NullGet.let(_lc31, Ref::get)))); }).all(); Ref<List<MapItemsInteractor>> _lc39 = () -> new ProvideBuilder<MapItemsInteractor>((_lc48) -> { _lc48.add(intractors().provideMapItemsInteractorSecond(_lc36, ListUtils.first(NullGet.let(_lc35, Ref::get)))); }).all(); MapViewModel _lc40 = viewmodels().provideMapViewModelSecond(ListUtils.first(NullGet.let(_lc39, Ref::get))); _lc25.add(_lc40); }).first()); }
Тысяча готова. И Еще на подходе
Мы в наших приложениях привыкли использовать компоненты в изолированных скоупах, ограниченных в рамках экранов, фрагментов, Activity или View. Для них можно делить, переносить и перетасовывать все новые компоненты, копировать множество архитектурных объектов сколько нужно. Но вот переиспользование таких компонентов в рамках локальных синглтонов становиться затруднительным. В одном компоненте DI просто так нельзя создавать, переиспользовать несколько экземпляров одного класса. Остается использовать независимые DI компоненты и использовать их через какой-нибудь менеджер этих самых компонентов. Немного выглядит как DI в DI.
В stone же можно использовать идентификаторы компонентов.
data class TripId( val tripId: String, ) @Component( identifiers = [ TripId::class ] ) interface AppComponent { // some code }
Идентификаторы позволяют теперь дублировать объекты в одном скоупе и обращаться к ним по идентификаторам.
@Module abstract class InteractorsModule { abstract fun provideTripInfoInteractor( tripId: TripId, ordersRepository: CurrentOrderRepository, tripInfoRepository: TripInfoRepository, ): TripInfoInteractor abstract fun provideMapItemsInteractor( tripId: TripId, ordersRepository: CurrentOrderRepository, tripInfoRepository: TripInfoRepository, ): MapItemsInteractor }

А все использование сводится к указанию нужного идентификатора на месте Inject'а объекта или его использования.
class MapScreen { @Inject lateinit var viewModel: MapViewModel init { DI.inject(mapScreen = this, tripId = TripId(argument.tripIdString)) } }
DI компонент может легко отличить аргументы провайдинга как зависимости или идентификаторы. И предоставлять все или отдельные зависимости по идентификатору.
Hidden text
DI подставляет идентификаторы по всей цепочке раскручивания зависимостей
@Override public void inject(MapScreen mapScreen, TripId tripId) { mapScreen.setViewModel(new ProvideBuilder<MapViewModel>((_lc0) -> { TripInfoInMemory _lc1 = data().provideTripInfoInMemory(tripId); TripInfoApi _lc3 = data().provideTripInfoApi(); CurrentOrderApi _lc5 = data().provideCurrentOrderApi(); Ref<List<CurrentOrderInMemory>> _lc8 = () -> new ProvideBuilder<CurrentOrderInMemory>((_lc20) -> { _lc20.add(data().provideCurrentOrderInMemory(tripId)); }).all(); Ref<List<TripInfoRepository>> _lc10 = () -> new ProvideBuilder<TripInfoRepository>((_lc21) -> { _lc21.add(repository().provideTripInfoRepository(tripId, _lc3, _lc1)); }).all(); CurrentOrderRepository _lc11 = repository().provideCurrentOrderRepository(tripId, _lc5, ListUtils.first(NullGet.let(_lc8, Ref::get))); Ref<List<MapItemsInteractor>> _lc14 = () -> new ProvideBuilder<MapItemsInteractor>((_lc23) -> { _lc23.add(intractors().provideMapItemsInteractor(tripId, _lc11, ListUtils.first(NullGet.let(_lc10, Ref::get)))); }).all(); MapViewModel _lc15 = viewmodels().provideMapViewModel(tripId, ListUtils.first(NullGet.let(_lc14, Ref::get))); _lc0.add(_lc15); }).first()); }
Сухой остаток
Одной из идей Stone - одна фабрика на все. И все было продумано, для того, чтобы избавиться от всяких фабрик и менеджеров-провайдеров viewModel'ей. А размытые скоупы в априори были призваны использовать минимальное кол-во DI скоупов на весь проект.
Ну а пока вы противитесь идеям развивать свой DI, вендоры оболочек Android будут и дальше зарабатывать на людях предлагая клонировать ваше приложение под несколько аккаунтов.
Заходите на wiki проекта и знакомьтесь с еще более продвинутыми фичами библиотеки.
