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. Subcomponents. Best practice
На хабре уже было несколько хороших статей по установке и работе с Dagger 2:
- 1 часть: Основы
- 2 часть: Subcomponent’ы
Я же хочу поделиться своим опытом использования Dagger 2 на реальных проектах с реальными кейсами. Раскрыть читателю мощь и удобство как самого Dagger’а, так и такого его аспекта, как Subcomponent.
Перед тем, как пройти под кат, следует ознакомиться с вышеуказанными статьями.
Кого заинтересовал, you are welcome!
Один мой друг научил меня отличному способу, как можно разложить всё по полочкам: представляя какую-либо архитектуру (либо отдельно взятый класс, либо даже небольшой кусок кода), попытайтесь перенести это в реальный мир. Найдите в повседневной жизни что-то схожее с логикой вашего кода. И тогда, опираясь на пример реальной жизни, вы поймете, как должен вести себя тот или иной программный компонент (объект). Поймете, какой результат должен в итоге получиться.
В этот раз я поступлю точно также.
Давайте отвлечемся от программирования и перенесемся в хирургический кабинет.
Спасение человеческих жизней — крайне ответственная задача. Каждый член бригады врачей должен безошибочно выполнять свою работу и не мешать другим выполнять свою.
На полках аккуратно разложены все инструменты. Главный врач сосредоточенно и кропотливо выполняет операцию, периодически обращаясь к ассистенту, чтобы получить новый инструмент, скажем, скальпель. В разные моменты времени может понадобиться «разный скальпель», и потому ассистенту важно также не отвлекаться от процесса и подавать именно тот инструмент, который в данный момент необходим.
Врач абсолютно не заботится о том, на какой полке лежит нужный ему инструмент. Для него важнее полностью сконцентрироваться на операции, дабы не допустить ошибок — это его зона ответственности.
Ассистент же отвечает за наличие всех необходимых в данной операции инструментов, за их чистоту, за своевременное предоставление инструментов врачу. Ну и самое интересное, ассистент самостоятельно решает в зависимости от ситуации, какой инструмент выбрать; конечно, если от врача не было точных указаний.
В нашем случае ассистент — это и есть Dagger. Врач — наш программный компонент, имеющий четкое предназначение в программе. Именно в делегировании (от врача ассистенту) создания и предоставления зависимостей (инструментов) и заключается паттерн — Dependency Injection (внедрение зависимости).
Что можно вынести из этого примера:
- Компонент не должен содержать в себе логику создания других компонентов.
Практика. Вернемся к программированию.
В подавляющем большинстве приложений есть работа с каким-либо списком данных.
За обработку данных отвечает адаптер, который принимает в конструктор:
- Listener — для событий взаимодействия с элементами списка;
- Возможно, контекст или LayoutInflater — для создания ViewHodler’ов;
- Ну и сам список данных, если, конечно, он был инициализирован заранее (иначе адаптер реализует свой метод setList()).
Но что в итоге? Получив в нашем Fragment’е (или Activity) конструкцию
Мы озаботили наш компонент инициализацией другого компонента. Наш врач отошел от операционного стола, чтобы найти нужный инструмент.
С Dagger’ом же мы не просто избавимся от первой строки представленного кода, а именно освободим компонент от логики создания другого компонента — от излишней для него логики.
Минуточку, здесь может появиться вопрос:
Если инициализацию адаптера делегировать Dagger’у, откуда он возьмет Listener (объект нашего компонента, реализующего Listener)? Хранить синглтон фрагмента или активити — это больше, чем плохая идея!
Такой вопрос может возникнуть, если Вы:
- Используете один-два Component’а для всего приложения;
- Все зависимости храните синглтонами;
- И знать не хотите про Subcomponent’ы и Component dependency.
Уйдем в небольшую абстракцию, которой мне не хватало на первых порах изучения Dagger.
Большинство примеров использования Dagger’а в «интернетах» обязательно включает в себя создание так называемого AppComponent’a с его AppModule’м с корневой зависимостью Context (либо вашим классом, расширяющим Application, что по сути тоже Context).
«В начале было слово. »
Имея Context, мы можем получить другие зависимости, например: SharedPreferences, LayoutInflater, какой-нибудь системный сервис и т.д. Соответственно, имея SharedPreferences, мы можем получить PreferenceHelper — класс-утилита для работы с преференсами. Имея LayoutInflater, можем получить какой-нибудь ViewFactory. Из этих «более высокоуровневых» зависимостей мы также можем получить еще и еще более сложные, комплексные. И всё это разнообразие пошло из одного только объекта — контекста. В данном случае его можно назвать ядром нашего AppComponent’а.
И всё вышеперечисленное — это как раз те зависимости, которые должны существовать на протяжении жизни всего приложения, т.е. Singleton’ы. Именно поэтому в качестве ядра у нас выступает тот объект, что существует всё это время — объект контекста приложения.
Продолжая эту мысль, подумаем, насколько долго должен существовать наш Adapter? Очевидно, пока существует экран, с которым этот адаптер работает.
Adapter’у мы предоставим ViewHolderFactory, которая должна существовать, пока существует Adapter. Помимо Adapter’а предоставим Fragment’у некоторый ViewController, и он также должен существовать, только пока существует Fragment, и т.д.
Если разобраться, все зависимости, используемые исключительно пока «жив» данный экран, от этого экрана и зависят. Т.о. можно сказать, что наш Fragment (или Activity) будет являться ядром нашего локального Component’а — Component’а, который существует, пока существует наш экран.
Чтобы реализовать четко определенное время жизни всей этой локальной кучке (графу) наших зависимостей, мы будем использовать Subcomponent.
Пока что забудем про приставку sub и представим, что мы реализуем просто Component. Если вам будет проще, представьте, что наш экран — это и есть всё наше приложение.
Начнем с того, что нам нужен базовый модуль. Т.к. наш экран со списком, назову его ListModule.
Теперь нам необходимо то самое ядро — базовая зависимость, от которой пойдут все остальные. Как говорилось ранее, базовой зависимостью для экрана является сам «объект экрана» — например, ListFragment. Передадим его в конструкторе модуля.
Основа есть, дальше творчество.
Предоставим наш адаптер:
NOTE: У нас есть Context, но явно мы его не предоставляли ни в этом модуле, ни в других модулях нашего Component’а. Об этом чуть позже.
Можно даже отдельно предоставить сам список данных (это избыточно, но для примера сойдет):
Теперь, чтобы всё заработало как надо, немного настроек.
Дабы подсказать Dagger’у, что:
- Все зависимости Component’а являют собой один граф, отдельный от основного;
- Мы хотим не создавать каждый раз новую зависимость, а кешировать единственную;
существуют так называемые Scope-аннотации. Выглядит каждая Scope-аннотация примерно так:
Singleton — это базовая аннотация, предоставляемая Dagger’ом. Предоставляется она просто для того, чтобы вам было, от чего отталкиваться. Само «singleton-ство» не будет происходить магическим образом, если вы не сохраните свой AppComponent в классе App (классе, расширяющем Application). Т.е. Dagger гарантирует вам, что для данного экземпляра Component’а будет создан единственный экземпляр зависимости. Но за единственность экземпляра Component’а вы отвечаете сами.
Подобным образом создадим свою scope-аннотацию:
Наша аннотация ничем не уступит аннотации Singleton, вся суть в том, как мы их используем.
Scope-аннотацией мы помечаем свои provide-методы и Component, содержащий наши модули.
ВАЖНО: В одном Component’е, подписанном определенным Scope’ом могут находиться только модули, provide-методы которых подписаны тем же самым Scope’ом. Т.о. мы не пересекаем два разных графа зависимостей.
Итоговый вид нашего ListModule:
И наш Component:
Ключевой здесь является аннотация @Subcomponent. Так мы сообщаем, что хотим иметь доступ ко всем зависимостям нашего родительского Component’а, но, заметьте, родителя здесь не указываем. В нашем примере родителем будет AppComponent.
* Именно из AppComponent’а мы получим Context для инициализации адаптера.
Чтобы получить свой Subcomponent, в родительском Component’е необходимо описать метод его получения, передав в аргументы все модули Subcomponent’а (в нашем случае только один модуль).
Как это выглядит:
Dagger позаботится о реализации этого метода.
Организуем время жизни
Как уже говорилось, AppComponent потому Singleton, что мы храним его единственный экземпляр в классе App. Создать экземпляр своего Subcomponent’а мы можем только с помощью родительского, а потому всю логику получения и хранения Subcomponent’а также вынесем в класс App, с одним важным отличием: Мы добавим возможность в любой момент создать Subcomponent, и в любой момент разрушить.
В классе App опишем следующую логику:
NOTE: На больших проектах имеет смысл выносить логику работы с Dagger’ом из класса App в класс-хэлпер, используя композицию.
Ну, и остается описать использование всего этого в нашем фрагменте:
Таким образом мы привязали время жизни нашего графа к жизненному циклу фрагмента.
Это может выглядеть излишним в случае с одной зависимостью (хотя даже с одной зависимостью вынос подобной логики делает ваш код более чистым и менее зацепленным). Бо’льшая часть работы заключается в создании архитектуры. А потому теперь, если вам понадобится предоставить новую зависимость, дело сведётся к реализации одного provide-метода.
БОНУС
Когда все зависимости выделены в provide-методы, появляется такая приятная плюшка, как избавление от прокидывания каких-либо зависимостей. Рассмотрим опять же на примере с адаптером.
ListFragment реализует Listener событий, связанных с ViewHolder-ами объектов нашего списка. Соответственно, чтобы доставить Listener каждому ViewHolder’у, появляется необходимость хранения ссылки на Listener в Adapter’е.
Избавимся от посредника.
Хорошей практикой считается вынос создания ViewHolder’ов во ViewHolderFactory. Так и поступим:
Наш модуль преобразится к такому виду:
NOTE: Не забываем предоставить LayoutInflater в AppModule.
Мне кажется, данный пример хорошо показывает, насколько гибкой становится работа с зависимостями.
А теперь представьте мир, в котором мы делаем код-ревью определенного компонента (класса), и видим только его логику. Нет необходимости «скакать» между программными компонентами, чтобы отследить нить событий. Внешние инструменты появляются у нас сами собой, а с другими компонентами наш взаимодействует через интерфейсы (или вообще не взаимодействует).
Надеюсь, эта статья дала Вам почву для размышлений и творчества, а мир Dagger’а стал хоть немного ближе.
В следующий раз разберем вторую часть функционала ассистента — возвращать определенную реализацию «скальпеля» в зависимости от ситуации. Поговорим об авторизованной зоне и работе с социальными сетями.
Источник