На хабре уже было несколько хороших статей по установке и работе с Dagger 2:
Я же хочу поделиться своим опытом использования Dagger 2 на реальных проектах с реальными кейсами. Раскрыть читателю мощь и удобство как самого Dagger’а, так и такого его аспекта, как Subcomponent.
Перед тем, как пройти под кат, следует ознакомиться с вышеуказанными статьями.
Кого заинтересовал, you are welcome!
Один мой друг научил меня отличному способу, как можно разложить всё по полочкам: представляя какую-либо архитектуру (либо отдельно взятый класс, либо даже небольшой кусок кода), попытайтесь перенести это в реальный мир. Найдите в повседневной жизни что-то схожее с логикой вашего кода. И тогда, опираясь на пример реальной жизни, вы поймете, как должен вести себя тот или иной программный компонент (объект). Поймете, какой результат должен в итоге получиться.
В этот раз я поступлю точно также.
Давайте отвлечемся от программирования и перенесемся в хирургический кабинет.
Спасение человеческих жизней — крайне ответственная задача. Каждый член бригады врачей должен безошибочно выполнять свою работу и не мешать другим выполнять свою.
На полках аккуратно разложены все инструменты. Главный врач сосредоточенно и кропотливо выполняет операцию, периодически обращаясь к ассистенту, чтобы получить новый инструмент, скажем, скальпель. В разные моменты времени может понадобиться «разный скальпель», и потому ассистенту важно также не отвлекаться от процесса и подавать именно тот инструмент, который в данный момент необходим.
Врач абсолютно не заботится о том, на какой полке лежит нужный ему инструмент. Для него важнее полностью сконцентрироваться на операции, дабы не допустить ошибок — это его зона ответственности.
Ассистент же отвечает за наличие всех необходимых в данной операции инструментов, за их чистоту, за своевременное предоставление инструментов врачу. Ну и самое интересное, ассистент самостоятельно решает в зависимости от ситуации, какой инструмент выбрать; конечно, если от врача не было точных указаний.
В нашем случае ассистент — это и есть Dagger. Врач — наш программный компонент, имеющий четкое предназначение в программе. Именно в делегировании (от врача ассистенту) создания и предоставления зависимостей (инструментов) и заключается паттерн — Dependency Injection (внедрение зависимости).
Что можно вынести из этого примера:
- Компонент не должен содержать в себе логику создания других компонентов.
- Компонент не должен заботиться о реализации своих инструментов. В нашем примере, если хирург попросит: «Скальпель!», ассистент по ситуации вернёт нужный из множества. Т.о. можно сказать, что врач работает не с конкретными реализациями инструментов, а с их интерфейсами.
Практика. Вернемся к программированию.
В подавляющем большинстве приложений есть работа с каким-либо списком данных.
За обработку данных отвечает адаптер, который принимает в конструктор:
- Listener — для событий взаимодействия с элементами списка;
- Возможно, контекст или LayoutInflater — для создания ViewHodler’ов;
- Ну и сам список данных, если, конечно, он был инициализирован заранее (иначе адаптер реализует свой метод setList()).
Но что в итоге? Получив в нашем Fragment’е (или Activity) конструкцию
Adapter adapter = new Adapter(this, getContext(), list);
recyclerView.setAdapter(adapter);
Мы озаботили наш компонент инициализацией другого компонента. Наш врач отошел от операционного стола, чтобы найти нужный инструмент.
С 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.
@Module
public class ListModule {
}
Теперь нам необходимо то самое ядро — базовая зависимость, от которой пойдут все остальные. Как говорилось ранее, базовой зависимостью для экрана является сам «объект экрана» — например, ListFragment. Передадим его в конструкторе модуля.
@Module
public class ListModule {
private final ListFragment fragment;
public ListModule(ListFragment fragment) {
this.fragment = fragment;
}
}
Основа есть, дальше творчество.
Предоставим наш адаптер:
@Provides
public Adapter provideAdapter(Context context) {
return new Adapter(fragment, context, fragment.initList());
}
NOTE: У нас есть Context, но явно мы его не предоставляли ни в этом модуле, ни в других модулях нашего Component'а. Об этом чуть позже.
Можно даже отдельно предоставить сам список данных (это избыточно, но для примера сойдет):
@Provides
public List<Model> provideListOfModels() {
return fragment.initList();
}
@Provides
public Adapter provideAdapter(Context context, List<Model> list) {
return new Adapter(fragment, context, list);
}
Теперь, чтобы всё заработало как надо, немного настроек.
Дабы подсказать Dagger’у, что:
- Все зависимости Component'а являют собой один граф, отдельный от основного;
- Мы хотим не создавать каждый раз новую зависимость, а кешировать единственную;
существуют так называемые Scope-аннотации. Выглядит каждая Scope-аннотация примерно так:
@Scope
@Retention(RetentionPolicy.Runtime)
public @interface Singleton {}
Singleton — это базовая аннотация, предоставляемая Dagger’ом. Предоставляется она просто для того, чтобы вам было, от чего отталкиваться. Само «singleton-ство» не будет происходить магическим образом, если вы не сохраните свой AppComponent в классе App (классе, расширяющем Application). Т.е. Dagger гарантирует вам, что для данного экземпляра Component'а будет создан единственный экземпляр зависимости. Но за единственность экземпляра Component'а вы отвечаете сами.
Подобным образом создадим свою scope-аннотацию:
@Scope
@Retention(RetentionPolicy.Runtime)
public @interface ListScope {}
Наша аннотация ничем не уступит аннотации Singleton, вся суть в том, как мы их используем.
Scope-аннотацией мы помечаем свои provide-методы и Component, содержащий наши модули.
ВАЖНО: В одном Component’е, подписанном определенным Scope’ом могут находиться только модули, provide-методы которых подписаны тем же самым Scope’ом. Т.о. мы не пересекаем два разных графа зависимостей.
Итоговый вид нашего ListModule:
@Module
public class ListModule {
private final ListFragment fragment;
public ListModule(ListFragment fragment) {
this.fragment = fragment;
}
@ListScope
@Provides
public List<Model> provideListOfModels() {
return fragment.initList();
}
@ListScope
@Provides
public Adapter provideAdapter(Context contex, List<Model> list) {
return new Adapter(fragment, context, list);
}
}
И наш Component:
@ListScope
@Subcomponent(modules = ListModule.class)
public interface ListComponent {
void inject(ListFragment fragment);
}
Ключевой здесь является аннотация @Subcomponent. Так мы сообщаем, что хотим иметь доступ ко всем зависимостям нашего родительского Component’а, но, заметьте, родителя здесь не указываем. В нашем примере родителем будет AppComponent.
* Именно из AppComponent’а мы получим Context для инициализации адаптера.
Чтобы получить свой Subcomponent, в родительском Component’е необходимо описать метод его получения, передав в аргументы все модули Subcomponent’а (в нашем случае только один модуль).
Как это выглядит:
@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {
ListComponent listComponent(ListModule listModule);
}
Dagger позаботится о реализации этого метода.
Организуем время жизни
Как уже говорилось, AppComponent потому Singleton, что мы храним его единственный экземпляр в классе App. Создать экземпляр своего Subcomponent’а мы можем только с помощью родительского, а потому всю логику получения и хранения Subcomponent’а также вынесем в класс App, с одним важным отличием: Мы добавим возможность в любой момент создать Subcomponent, и в любой момент разрушить.
В классе App опишем следующую логику:
public class App extends Application {
private ListComponent listComponent;
public ListComponent initListComponent(ListFragment fragment) {
listComponent = appComponent.listComponent(new ListModule(fragment));
return listComponent
}
public ListComponent getListComponent() {
return listComponent;
}
public void destroyListComponent() {
listComponent = null;
}
}
NOTE: На больших проектах имеет смысл выносить логику работы с Dagger’ом из класса App в класс-хэлпер, используя композицию.
Ну, и остается описать использование всего этого в нашем фрагменте:
public class ListFragment extends Fragment {
@Inject
Adapter adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
App.getInstance().initListComponent(this).inject(this);
init();
}
private void init() {
recyclerView.setAdapter(adapter);
}
@Override
public void onDestroy() {
super.onDestroy();
App.getInstance.destroyListComponent();
}
}
Таким образом мы привязали время жизни нашего графа к жизненному циклу фрагмента.
Это может выглядеть излишним в случае с одной зависимостью (хотя даже с одной зависимостью вынос подобной логики делает ваш код более чистым и менее зацепленным). Бо’льшая часть работы заключается в создании архитектуры. А потому теперь, если вам понадобится предоставить новую зависимость, дело сведётся к реализации одного provide-метода.
БОНУС
Когда все зависимости выделены в provide-методы, появляется такая приятная плюшка, как избавление от прокидывания каких-либо зависимостей. Рассмотрим опять же на примере с адаптером.
ListFragment реализует Listener событий, связанных с ViewHolder-ами объектов нашего списка. Соответственно, чтобы доставить Listener каждому ViewHolder’у, появляется необходимость хранения ссылки на Listener в Adapter’е.
Избавимся от посредника.
Хорошей практикой считается вынос создания ViewHolder’ов во ViewHolderFactory. Так и поступим:
public class ListItemViewHolderFactory {
private final Listener listener;
private final LayoutInflater layoutInflater;
public ListItemViewHolderFactory(LayoutInflater layoutInflater, Listener listener) {
this.layoutInflater = layoutInflater;
this.listener = listener;
}
public ListItemViewHolder createViewHolder(ViewGroup parent) {
View view = layoutInflater.inflate(R.layout.item, parent, false);
return new ListItemViewHolder(view, listener);
}
}
Наш модуль преобразится к такому виду:
@Module
public class ListModule {
private final ListFragment fragment;
public ListModule(ListFragment fragment) {
this.fragment = fragment;
}
@ListScope
@Provides
public List<Model> provideListOfModels() {
return fragment.initList();
}
@ListScope
@Provides
public Adapter provideAdapter(ListItemViewHolderFactory factory,
Context context,
List<Model> list) {
return new Adapter(factory, context, list);
}
@ListScope
@Provides
public ListItemViewHolderFactory provideVhFactory(LayoutInflater layoutInflater) {
return new ListItemViewHolderFactory (layoutInflater, fragment);
}
}
NOTE: Не забываем предоставить LayoutInflater в AppModule.
Мне кажется, данный пример хорошо показывает, насколько гибкой становится работа с зависимостями.
А теперь представьте мир, в котором мы делаем код-ревью определенного компонента (класса), и видим только его логику. Нет необходимости «скакать» между программными компонентами, чтобы отследить нить событий. Внешние инструменты появляются у нас сами собой, а с другими компонентами наш взаимодействует через интерфейсы (или вообще не взаимодействует).
Надеюсь, эта статья дала Вам почву для размышлений и творчества, а мир Dagger’а стал хоть немного ближе.
В следующий раз разберем вторую часть функционала ассистента — возвращать определенную реализацию «скальпеля» в зависимости от ситуации. Поговорим об авторизованной зоне и работе с социальными сетями.
Спасибо за внимание.
Пример, описанный в статье, на гитхабе.
Автор: Центр Высоких Технологий