- Dagger 2: Scopes and Subcomponents
- Dagger 2. Часть первая. Основы, создание графа зависимостей, Scopes
- Теория
- Принципы инверсии управления
- Недостатки дизайна, которые устраняются с применением Инверсии управления
- Внедрение зависимости (DI)
- Преимущества Dagger 2
- Основные элементы(аннотации) Dagger 2:
- Практика
- Создание синглтонов
Dagger 2: Scopes and Subcomponents
Update: Dagger for Android guide available here.
This is the 2nd part of the Dagger 2 Dependency Injection tutorial series. First article can be found here.
In the last article, we talked about the basics of Dagger 2 and how to use it for simple use cases. Now we are going to talk about scopes and its role in lifecycle and subcomponents.
If your design follows the clean architecture pattern, you may have a structure similar to this:
Activities have their own Presenters or ViewModels, 1 or more Presenters or ViewModels may require a single Interactor and Interactors depends on the data layer. This separation of concern approach makes horizontal levels fairly easy to recognize. We can take advantage of two things here:
- Dependency scopes can be localized
- Lifecycle can be localized
We need localization because we do not want all our dependencies to live as long as the application, and there are cases when we want our dependencies to not share the same state by being the same object.
Dagger 2 provides @Scope as a mechanism to handle scoping. Scoping allows you to “preserve” the object instance and provide it as a “local singleton” for the duration of the scoped component.
In the last tutorial, we discussed a special scope called @Singleton. We agreed that @Singleton allows you to define a component or a dependency as a global object. Well that is not entirely true.
@Singleton, is just another scope. It was just there as the default scoping annotation. It provides your dependencies as a singleton as long as you are using the same component.
Let’s take this slow as I think the concept is not pretty straightforward. First, let us define our own scope.
We define a custom scope by using @Scope. We will create an ApplicationScope that is similar to @Singleton to prove that the latter is just another scope.
First we will create an unscoped component.
We defined a data class Warrior with a name property. This will serve as our dependency. Next we define a module that provides this dependency. But everytime a new warrior is instantiated, we increment the index. This will help us to figure out if indeed a new instance is created every time. Finally a component that exposes this dependency. Then let us instantiate in our application.
Running our app will output these logs:
This is expected as the component and dependency is unscoped. Now let us annotate our component and provide method with our new scope.
Running our application will now output the following logs
Notice that the same instance is returned to us. This is the power of scoping. It provides us local singletons within the defined scope.
Now, the scope is only valid within the component. When a new component of the same type is initialized, it will have a new sets of dependencies. This is the reason why @Singleton components are instantiated and maintained in the Application class. The singleton application component should persist throughout the application lifecycle, and the app should refer to this component to uphold that “single instance” requirement.
Following from our architecture design, we can therefore define each layer as a scope.
Now that we have defined the different scopes, we need to find a way to establish a relationship between our components. There are 2 ways and we will discuss them both.
A component can establish a parent-child dependency between itself and other components. For this to work, the parent components should expose their child’s dependency. Let’s take a look at an example.
Suppose you have an app that follows the MVP design pattern (More on MVP here). You want your WarriorActivity to be injected with a WarriorPresenter. This presenter is designed for the WarriorActivity only so we can scope this as @WarriorScreenScope.
First we define a presenter that accepts a warrior instance. Then we define another custom scope @WarriorScreenScope. Next we define a module that provides a warrior presenter. Note that we don’t want to create our own warrior but we want the app component to provide it for us. Now the important part. We establish the dependency to other components using the dependencies property of the component. Multiple components are supported. The only requirement is that your parent components should expose your required dependency, which in our case, the warrior object. Fortunately it is already exposed for us.
Now let us instantiate our WarriorScreenComponent and inject our WarriorPresenter to the activity.
First we need to instantiate the parent components and pass their instances in our component. The above example is just to simplify things but the best approach as discussed previously is to save the instance of your global components and use them in you child components. This will allow you to use the scoped objects in those components. Running the activity will output this which is as expected.
Subcomponent is another way of building component relationships. This can be thought of as something similar in concept to inner/outer class relationship between classes in OOP (Thanks to Farid Mammadov for pointing this out). A component can only have 1 parent while a parent can be depended to by multiple components. Let’s take a look as how to use subcomponent.
The parent should provide a method to get their child components. The code above exposes the WarriorScreenComponent from the AppComponent. This will allow us to instantiate WarriorScreenComponent from AppComponent.
Now the child component.
The child component is basically the same with the exception of the @Subcomponent annotation. This specifies that this component is a subcomponent. Note that a subcomponent can only have one parent. you cannot specify your parent actually. Your parent (or the parent of its parent) should ensure that it has all its child’s dependency (aside from the modules of course).
Now instantiating is pretty straightforward.
You can get the subcomponents from the parent components (and providing the parameters of course).
That concludes our scopes tutorial. Watch out for future articles regarding dagger 2. Hope you enjoyed this one.
Источник
Dagger 2. Часть первая. Основы, создание графа зависимостей, Scopes
Всем привет! В последнее время появилось много средств, библиотек, которые существенно облегчают написание кода под Android. Только успевай за всем следить и все пробовать. Одним из таких средств является библиотека Dagger 2.
В сети уже много различного материала, посвященного данной библиотеке. Но когда я только начинал ознакамливаться с Dagger 2, читал статьи, смотрел доклады, я во всем этом находил один общий недостаток — мне, как человеку, не работавшему со Springом и прочими подобными фреймворками/библиотеками, было довольно сложно понять, откуда берутся зависимости, как они «провайдятся» и что вообще там происходит. На слушателей/читателей обычно сразу «вываливается» большое количество кода с новыми аннотациями. И это как-то работало. В итоге, после доклада/статьи в голове все никак не могло сложиться в единую понятную картину.
Сейчас, оглядываясь назад, я понимаю, что мне тогда очень не хватало схематичного отображения, картинок, явно показывающих «что, откуда и куда». Поэтому в своем цикле статей я постараюсь восполнить данный пробел. Надеюсь, это поможет новичкам и всем заинтересованным лучше понять Dagger 2 и решиться попробовать его у себя в проекте. Могу сразу сказать, это стоит того.
И да, изначально я хотел написать одну статью, но материала и картинок вышло уж как-то много, поэтому информацию я буду выкладывать небольшими порциями, чтобы читатель мог постепенно погружаться в тему.
Теория
Быстро пробежимся по теоретическим аспектам.
Dagger 2 представляет собой библиотеку, которая помогает разработчику реализовать паттерн Внедрение зависимости (Dependency Injection), который в свою очередь является «специфичной формой инверсии управления (Inversion of control)».
Принципы инверсии управления
Недостатки дизайна, которые устраняются с применением Инверсии управления
Внедрение зависимости (DI)
Процесс предоставления внешней зависимости программному компоненту. Является специфичной формой «инверсии управления» (англ. Inversion of control, IoC), когда она применяется к управлению зависимостями. В полном соответствии с принципом единой обязанности объект отдаёт заботу о построении требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму.
Так вот, Dagger 2 как раз и берет на себя заботу создания этого общего механизма.
Предчувствуя вопросы и холивары по IoC, DI, как они соотносятся друг с другом, я добавлю, что определения были взяты с Википедии, и подробное обсуждение выходит за рамки статьи.
Теперь перечислим основные преимущества библиотеки.
Преимущества Dagger 2
Для примера простого доступа к “расшаренным” реализациям приведу код:
То есть к полям добавляются аннотации @Inject и в метод onCreate добавляется строчка App.getComponent().inject(this); . И теперь классу MainActivity доступны готовые реализации RxUtilsAbs и NetworkUtils .
Все эти вышеперечисленные преимущества делают Dagger 2 лучшей библиотекой для реализации DI в Android на данный момент.
Конечно, у библиотеки есть и недостатки. Но о них мы поговорим уже в конце цикла статей. Сейчас-то моя задача заинтересовать вас и подтолкнуть попробовать Dagger 2.
Основные элементы(аннотации) Dagger 2:
Пока что просто просмотрите данные аннотации для общего ознакомления. Каждую из них мы подробно обсудим.
Собственно, по теории ограничимся этим. Более подробно можно ознакомиться по ссылкам в конце статьи.
Основная же наша цель — это понять, как происходит построение всего графа зависимостей с помощью Dagger 2.
Практика
Теперь начинаются уже более интересные вещи.
Рассмотрим конкретный пример. У всех в приложениях есть синглтоны. В Android без них никуда, учитывая жизненные циклы активити и фрагментов.
При этом имеющиеся синглтоны я бы разделил на две категории:
- «Глобальные» синглтоны, которые могут понадобиться в любой части приложения. К ним относятся Context, утилитные классы и прочие классы, влияющие на работу всего приложения
- «Локальные» синглтоны, которые нужны только в определенном одном или нескольких модулях. Но из-за возможных переориентаций экрана и прочего, часто возникает необходимость выноса части логики и данных в независимое от жизненного цикла место. О «локальных» синглтонах более подробно и схематично в следующей статье.
Начнем с «глобальных» синглтонов. Как обычно мы их используем? Смею предположить, что в большинстве имеет место следующий код:
Обычная практика. Но если мы хотим применять у себя паттерн DI, то данный код будет не удовлетворителен по нескольким причинам:
- В классе, где используется подобный вызов, внезапно возникает зависимость от класса SomeSingleton . Это неявная зависимость, она нигде четко не обозначена (ни в конструкторе, ни в полях, ни в методах). Поэтому увидеть такую зависимость можно только просматривая код конкретного метода, а ведь по интерфейсу класса и не скажешь, что здесь применяется данный SomeSingleton .
- Процессом инициализации занимается сам SomeSingleton . А если используется ленивая инициализация, то стартует процесс инициализации какой-нибудь из классов, применяющих SomeSingleton (где первым вызовется). То есть классы, помимо своей работы, отвечают еще и за старт инициализации Синглтона.
- С увеличением количества таких Синглтонов система покрывается сетью неявных зависимостей. Еще одни синглтоны могут зависеть от других, что также не упрощает дальнейшее их сопровождение. Плюс синглтоны разбросаны по системе, могут находится в разных пакетах, и это причиняет некоторые неудобства.
С этим всем, конечно же, можно жить. Нелегко, но можно. Но все начинает в корне меняться, когда вы захотите обложить свой код юнит-тестами. Вот тут вам придется что-то делать с этими неявными зависимостями, как-то совершать их корректную «подмену». Вы начинаете волей-неволей преобразовывать свой код в «тестируемый код», а с неявными зависимостями — это нереально.
А теперь о Dagger 2 (по ходу статьи я буду иногда называть ее просто по-русски — «Даггер»). Сейчас мы увидим, как с помощью Dagger 2 можно реализовать синглтоны по DI. А заодно увидим весь цикл создания графа зависимостей.
Начнем с «глобальных» синглтонов.
Создание синглтонов
Как мы помним, @Module — это аннотация, помечающий класс, чьи методы «предоставляют зависимости» («провайдят зависимости»). В дальнейшем подобные классы мы будем называть просто Модулями. А методы, которые «провайдят зависимости» или «предоставляют зависимости», будем называть provide-методами.
К примеру в ReceiversModule есть метод provideNetworkChannel , который как раз и предоставляет объект типа NetworkChannel . Данный метод на самом деле может называться как угодно, самое главное — аннотация @Provides перед методом и возвращаемый тип ( NetworkChannel ).
Распространена практика, когда возвращаемый тип — это интерфейс или абстрактный класс ( RxUtilsAbs ), а внутри метода мы уже инициализируем и возвращает нужную реализацию ( RxUtils ).
Про аннотацию @Singleton ниже, пока не обращаем внимания на нее.
Также в модуль, в конструктор, можно передавать необходимые объекты. Пример — AppModule .
А с UtilsModule уже интереснее. Чтобы предоставить свои зависимости — RxUtilsAbs и NetworkUtils ему необходимы объекты типов Context и NetworkChannel . Значит мы должны как-то сказать Даггеру, что при создании объектов RxUtilsAbs и NetworkUtils необходимы Context и NetworkChannel . Для этого в методы provideRxUtils и provideNetworkUtils добавляются аргументы: Context context для первого и Context context, NetworkChannel networkChannel для второго.
При этом название аргументов может быть любое, хоть context , хоть contextSuper , без разницы. Главное, это типы аргументов.
Далее создаем интерфейс AppComponent с аннотацией
@Component(modules =
Для удобства подобный интерфейс мы будем называть Компонентом.
Как говорилось выше, @Component по сути является мостом между @Module и @Inject . Или другими словами, Компонент представляет собой готовый граф зависимостей. Что это означает? Поймем чуть ниже.
Данной аннотацией мы говорим Даггеру, что AppComponent содержит три модуля — AppModule, UtilsModule, ReceiversModule . Зависимости, которые провайдит каждый из этих модулей, доступны для всех остальных модулей, объединенных под эгидой компонента AppComponent . Для большей наглядности взглянем на рисунок.
Я думаю, с помощью этого рисунка станет намного понятней, откуда Даггер берет объекты Context и NetworkChannel для построения RxUtilsAbs и NetworkUtils . Если из аннотации компонента убрать модуль AppModule , например, то при компиляции Даггер заругается и спросит, откуда ему взять объект Context .
Также внутри интерфейса объявляем метод void inject(MainActivity mainActivity) . Этим методом мы сообщаем Даггеру, в какой класс/классы мы хотим делать инъекции.
Добавлю, что если необходимо заинжектить зависимости еще в какой-нибудь класс, помимо MainActivity (например, в SecondActivity ), то это должно быть четко прописано в интерфейсе. Например,
Название аргументов может быть любым ( mainActivity можно поменять на activity и т.д.). Самое главное тип объекта, куда мы собираемся инжектить! И нельзя использовать для всех классов, в которые «прокидываем зависимости», какое-нибудь обобщение типа:
Потому что Dagger 2 работает на кодогенерации, не рефлексии! Типы всегда должны указываться четко!
Едем дальше. Помните, мы в AppComponent целью инъекций задали класс MainActivity . В данном классе мы можем использовать те зависимости, которые провайдят модули AppModule, UtilsModule, ReceiversModule . Для этого нужно просто добавить в класс соответствующие поля и пометить их аннотацией @Inject , а также сделать их доступность как минимум пакетной (если поле задано как private , то Даггер не сможет подставить в это поле нужную реализацию).
Также отмечу, что в поле RxUtilsAbs rxUtilsAbs подставляется класс RxUtils ( RxUtils — наследник RxUtilsAbs ), то есть то, что мы и задали в модуле UtilsModule .
Далее в методе onCreate мы добавляем строчку
App.getComponent().inject(this);
Так как рассматриваем мы создание синглтонов, то компонент наш AppComponent лучше хранить в классе Application . В нашем примере получить доступ к AppComponent можно через App.getComponent() .
Вызывая же метод inject(MainActivity mainActivity) , мы окончательно связываем наш граф зависимостей. Таким образом, все зависимости, которые провайдят модули AppComponent ( Context , NetworkChannel , RxUtilsAbs , NetworkUtils ), становятся доступны в MainActivity .
Обратим внимание на метод buildComponent() класса App . DaggerAppComponent до компиляции нам не доступен.
Поэтому в начале не обращаем внимания на IDE, которая будет говорить, что класса DaggerAppComponent не существует. Ну и еще IDE не будет подсказывать при построении билдера. Так что инициализацию AppComponent с поомощью DaggerAppComponent придется писать «вслепую» первый раз.
Кстати код buildComponent() можно сократить:
Dagger 2, как мы уже говорили, отвечает за создание всего графа зависимости. Если что-то пойдет не так, он вам сообщит при компиляции. Никаких неожиданных и непонятных падений в рантайме, как это было, например, с Dagger 1.
А теперь внимание на схему ниже!
Фуф, теперь можно выдохнуть! Наиболее насыщенная часть позади. Мне кажется, данная схема наглядно демонстрирует, что:
- Модуль провайдит зависимости. То есть именно в модулях мы прописываем, какие объекты хотим предоставлять.
- Компонент являет собой граф зависимостей. Он объединяет модули и предоставляет зависимости нуждающимся классам ( MainActivity )
Если что-то не понятно или не явно, пишите в комментариях, исправим и объясним!
Ну и напоследок рассмотрим аннотацию @Singleton . Это Scope-аннотация, предоставляемая Даггером. Если перед методом, который провайдит зависимость, поместить @Singleton , то Даггер при инициализации Компонента, создаст единственный экземпляр помеченной зависимости, то есть синглтон. И при каждом затребовании данной зависимости будет предоставлять этот единственный экземпляр.
Меньше слов, больше картинок!
Каждая зависимость провайдится с аннотацией @Singleton . Это значит, что каждый раз, когда Даггеру необходимо будет использовать данную зависимость, он будет использовать только ее один экземпляр.
Теперь для сравнения уберем у метода provideNetworkChannel аннотацию @Singleton (зависимость становится «unscoped»). Это значит, что когда Даггеру необходимо будет использовать данную зависимость, он будет каждый раз создавать новый ее экземпляр.
Мы можем также создавать кастомные Scope-аннотации (подробнее в следующей статье).
Приведем некоторые особенности Scope-аннотаций:
- Обычно scope-аннотации задаются для Компонента и provide-метода.
- Если хоть один provide-метод имеет scope-аннотацию, то Компонент должен иметь точно такую же scope-аннотацию.
- Компонент может быть «unscoped», только если во всех его модулях все provide-методы также «unscoped».
- Все scope-аннотации в рамках одного компонента (то есть для всех модулей с provide-методами, входящих в состав Компонента, и у самого Компонента) должны быть одинаковыми.
Более подробно тема со Scope-аннотациями будет раскрыта в следующей статье. А для начала нам хватит и этого 🙂
Итак, в данной статье мы ознакомились с теоретическими аспектами IoC, DI, Dagger 2. Рассмотрели подробно создание графа зависимостей с помощью Dagger 2, частично познакомились со scope-аннотациями и ее конкретной реализацией @Singleton .
Привожу список статей, которые рекомендую к прочтению:
Источник