Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Использование библиотеки 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 проекта и знакомьтесь с еще более продвинутыми фичами библиотеки.