На хабре уже было несколько хороших статей по установке и работе с Dagger 2:


Я же хочу поделиться своим опытом использования Dagger 2 на реальных проектах с реальными кейсами. Раскрыть читателю мощь и удобство как самого Dagger’а, так и такого его аспекта, как Subcomponent.

Перед тем, как пройти под кат, следует ознакомиться с вышеуказанными статьями.
Кого заинтересовал, you are welcome!
image

Один мой друг научил меня отличному способу, как можно разложить всё по полочкам: представляя какую-либо архитектуру (либо отдельно взятый класс, либо даже небольшой кусок кода), попытайтесь перенести это в реальный мир. Найдите в повседневной жизни что-то схожее с логикой вашего кода. И тогда, опираясь на пример реальной жизни, вы поймете, как должен вести себя тот или иной программный компонент (объект). Поймете, какой результат должен в итоге получиться.

В этот раз я поступлю точно также.

Давайте отвлечемся от программирования и перенесемся в хирургический кабинет.

Спасение человеческих жизней — крайне ответственная задача. Каждый член бригады врачей должен безошибочно выполнять свою работу и не мешать другим выполнять свою.

На полках аккуратно разложены все инструменты. Главный врач сосредоточенно и кропотливо выполняет операцию, периодически обращаясь к ассистенту, чтобы получить новый инструмент, скажем, скальпель. В разные моменты времени может понадобиться «разный скальпель», и потому ассистенту важно также не отвлекаться от процесса и подавать именно тот инструмент, который в данный момент необходим.

Врач абсолютно не заботится о том, на какой полке лежит нужный ему инструмент. Для него важнее полностью сконцентрироваться на операции, дабы не допустить ошибок — это его зона ответственности.

Ассистент же отвечает за наличие всех необходимых в данной операции инструментов, за их чистоту, за своевременное предоставление инструментов врачу. Ну и самое интересное, ассистент самостоятельно решает в зависимости от ситуации, какой инструмент выбрать; конечно, если от врача не было точных указаний.

В нашем случае ассистент — это и есть Dagger. Врач — наш программный компонент, имеющий четкое предназначение в программе. Именно в делегировании (от врача ассистенту) создания и предоставления зависимостей (инструментов) и заключается паттерн — Dependency Injection (внедрение зависимости).

Что можно вынести из этого примера:

  1. Компонент не должен содержать в себе логику создания других компонентов.

  2. Компонент не должен заботиться о реализации своих инструментов. В нашем примере, если хирург попросит: «Скальпель!», ассистент по ситуации вернёт нужный из множества. Т.о. можно сказать, что врач работает не с конкретными реализациями инструментов, а с их интерфейсами.

Практика. Вернемся к программированию.

В подавляющем большинстве приложений есть работа с каким-либо списком данных.
За обработку данных отвечает адаптер, который принимает в конструктор:

  1. Listener — для событий взаимодействия с элементами списка;
  2. Возможно, контекст или LayoutInflater — для создания ViewHodler’ов;
  3. Ну и сам список данных, если, конечно, он был инициализирован заранее (иначе адаптер реализует свой метод setList()).

Но что в итоге? Получив в нашем Fragment’е (или Activity) конструкцию

Adapter adapter = new Adapter(this, getContext(), list);
recyclerView.setAdapter(adapter);

Мы озаботили наш компонент инициализацией другого компонента. Наш врач отошел от операционного стола, чтобы найти нужный инструмент.

image


С Dagger’ом же мы не просто избавимся от первой строки представленного кода, а именно освободим компонент от логики создания другого компонента — от излишней для него логики.

Минуточку, здесь может появиться вопрос:

Если инициализацию адаптера делегировать Dagger’у, откуда он возьмет Listener (объект нашего компонента, реализующего Listener)? Хранить синглтон фрагмента или активити — это больше, чем плохая идея!

Такой вопрос может возникнуть, если Вы:

  1. Используете один-два Component'а для всего приложения;
  2. Все зависимости храните синглтонами;
  3. И знать не хотите про Subcomponent’ы и Component dependency.

Уйдем в небольшую абстракцию, которой мне не хватало на первых порах изучения Dagger.

Большинство примеров использования Dagger’а в «интернетах» обязательно включает в себя создание так называемого AppComponent’a с его AppModule’м с корневой зависимостью Context (либо вашим классом, расширяющим Application, что по сути тоже Context).

Разберемся, почему.

«В начале было слово...»

Имея Context, мы можем получить другие зависимости, например: SharedPreferences, LayoutInflater, какой-нибудь системный сервис и т.д. Соответственно, имея SharedPreferences, мы можем получить PreferenceHelper — класс-утилита для работы с преференсами. Имея LayoutInflater, можем получить какой-нибудь ViewFactory. Из этих «более высокоуровневых» зависимостей мы также можем получить еще и еще более сложные, комплексные. И всё это разнообразие пошло из одного только объекта — контекста. В данном случае его можно назвать ядром нашего AppComponent’а.

image

И всё вышеперечисленное — это как раз те зависимости, которые должны существовать на протяжении жизни всего приложения, т.е. Singleton’ы. Именно поэтому в качестве ядра у нас выступает тот объект, что существует всё это время — объект контекста приложения.

Продолжая эту мысль, подумаем, насколько долго должен существовать наш Adapter? Очевидно, пока существует экран, с которым этот адаптер работает.

Adapter’у мы предоставим ViewHolderFactory, которая должна существовать, пока существует Adapter. Помимо Adapter’а предоставим Fragment’у некоторый ViewController, и он также должен существовать, только пока существует Fragment, и т.д.

Если разобраться, все зависимости, используемые исключительно пока «жив» данный экран, от этого экрана и зависят. Т.о. можно сказать, что наш Fragment (или Activity) будет являться ядром нашего локального Component’а — Component'а, который существует, пока существует наш экран.

image

Чтобы реализовать четко определенное время жизни всей этой локальной кучке (графу) наших зависимостей, мы будем использовать 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’у, что:

  1. Все зависимости Component'а являют собой один граф, отдельный от основного;
  2. Мы хотим не создавать каждый раз новую зависимость, а кешировать единственную;

существуют так называемые 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-метода.
image

БОНУС


Когда все зависимости выделены в provide-методы, появляется такая приятная плюшка, как избавление от прокидывания каких-либо зависимостей. Рассмотрим опять же на примере с адаптером.

ListFragment реализует Listener событий, связанных с ViewHolder-ами объектов нашего списка. Соответственно, чтобы доставить Listener каждому ViewHolder’у, появляется необходимость хранения ссылки на Listener в Adapter’е.

Избавимся от посредника.

image


Хорошей практикой считается вынос создания 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.

Мне кажется, данный пример хорошо показывает, насколько гибкой становится работа с зависимостями.

А теперь представьте мир, в котором мы делаем код-ревью определенного компонента (класса), и видим только его логику. Нет необходимости «скакать» между программными компонентами, чтобы отследить нить событий. Внешние инструменты появляются у нас сами собой, а с другими компонентами наш взаимодействует через интерфейсы (или вообще не взаимодействует).

image

Надеюсь, эта статья дала Вам почву для размышлений и творчества, а мир Dagger’а стал хоть немного ближе.

В следующий раз разберем вторую часть функционала ассистента — возвращать определенную реализацию «скальпеля» в зависимости от ситуации. Поговорим об авторизованной зоне и работе с социальными сетями.

Спасибо за внимание.

Пример, описанный в статье, на гитхабе.

Комментарии (11)


  1. r_ii
    03.08.2017 19:20

    В реальной жизни нету понятия синглтон — у каждой вещи (объекта) должен быть владелец. И в разработке будет намного лучше если выбросить этот антипаттерн из головы.
    Я понимаю если целью использования Dagger является облегчение тестирование, но не уловил сути использования его в статье.


    1. htc-cs Автор
      03.08.2017 21:22
      +1

      Цель использования Dagger в рамках данной статьи — это чистота и модульность кода. А уже из этих двух составляющих следуют такие преимущества, как тестируемость, переиспользуемость и т.д.


  1. nick1990spb
    03.08.2017 21:22

    По неведомой причине не отображаются первые картинки… Возможно, это только у меня, но проверьте, пожалуйста доступность для обитателей хабра вне Вашей сети…


    1. htc-cs Автор
      03.08.2017 21:23

      Постарались оперативно исправить


  1. dakatso
    04.08.2017 09:56

    Зачем ListComponent хранить в Application, если он должен разрушатся вместе с фрагментом, может стоит хранить его во фрагменте?


    1. htc-cs Автор
      04.08.2017 10:01

      К ListComponent может обращаться не только сам фрагмент, но и другие «участники» уровня данного экрана.
      Именно для такого совместного использования мы описали в классе App метод getListComponent().
      Например, тому же Adapter'у может понадобиться собственный метод inject() вместо передачи зависимостей в конструктор. В таком случае его вызов будет App.getListComponent().inject(this);

      Если же хранить ListComponent во фрагменте, появляется необходимость хранения instance фрагмента,
      как статической переменной класса ListFragment, чтобы обеспечить общий доступ. И даже если не углубляться, в то, почему так категорически не стоит делать, наш компонент все равно не разрушится сам, т.к. появляется необходимость занулять instance фрагмента руками.


      1. frankie
        09.08.2017 17:28

        Смысла хранить сабкомпоненты экранов я не вижу в принципе, время жизни таких компонентов равно времени жизни активити/фрагмента.

        К ListComponent как раз таки должен обращаться только фрагмент. Остальные зависимости данного скоупа ничего знать про компонент не должны, соответственно, в адаптере собственного метода inject() тоже быть не должно. Все его внутренние зависимости можно предоставить либо через модуль, либо повесить Inject на конструктор + методы/филды.


  1. kamer
    04.08.2017 15:22

    Следует отметить, что для такой реализации сабкомпонентов стоит воспользовать расширением для андроида https://google.github.io/dagger//android.html


  1. abbath0767
    08.08.2017 13:12

    А как поступать в случае архитектуры похожей на mvp? В любом случае большая часть зависимостей даггера будет прокидываться именно в презентер, а не в fragment/acrtivity и компонент будет выглядеть очень аляписто на мой взгляд если inject методы компонента будут содержать аргументы как презентеров так и вью (activity/fragment). Или я ошибаюсь и это будет корректным подходом?
    Просто иных выходов для предоставления зависимости андроид компонентов я не вижу (ну кроме как прокидывать в презентер адаптер, что не есть хорошо со стороны mvp)


    1. htc-cs Автор
      09.08.2017 11:16

      Если ваша архитектура подразумевает разделение ui от business-логики, отличным решением будет также разделить ваши Singletone-зависимости на два компонента — ui и business соответственно. С помощью Component dependency предоставляем из ui-компонента business-компоненту только необходимые общие зависимости (например Context) и интерфейсы для взаимодействия с вашими View (если, конечно, вы не используете фреймворки на типо Mosby).
      Таким образом вы добьетесь независимости ваших слоёв друг от друга + разграничите их доступ к layer-специфичным зависимостям.

      В общем-то, сочетание Dagger с различными архитектурами — это, наверно, тема для отдельной статьи. (Если, конечно, кому-то это будет интересно)