- Dagger 2 для начинающих Android разработчиков. Внедрение зависимостей. Часть 1
- Серия статей
- Ранее в цикле статей
- Что такое внедрение зависимостей (dependency injection, DI)
- Стратегия решения проблемы сильных связей (hard dependency) или проблемы Белых Ходоков
- Метод внедрения зависимостей
- Пример 1
- Сценарий 1. Без использования внедрения зависимостей
- Анализируем War
- Резюме
- Dagger & Android
- Philosophy
- Why Dagger on Android is hard
- dagger.android
- Injecting Activity objects
- How did that work?
- Injecting Fragment objects
- Base Framework Types
- Support libraries
- Dagger 2 и структура приложения для Android
- Почему Dagger 2
- Виды @ Inject
- @ Module
- @ Component
- @ Scope
- Уровень приложения
- Уровень Activity
- Уровень Fragment
- Lazy‹T› и Provider ‹T›
- Producer
Dagger 2 для начинающих Android разработчиков. Внедрение зависимостей. Часть 1
Данная статья является второй частью серии статей, предназначенных, по словам автора, для тех, кто не может разобраться с внедрением зависимостей и фреймворком Dagger 2, либо только собирается это сделать. Оригинал написан 25 ноября 2017 года. Изображения и GIF — из оригинала. Перевод вольный.
Это вторая статья цикла «Dagger 2 для начинающих Android разработчиков.». Если вы не читали первую, то вам сюда.
Серия статей
Ранее в цикле статей
В последней статье мы обсуждали зависимости. Мы узнали, что такое зависимость, обсудили их типы, влияние на процесс разработки и поддерживаемость проекта.
Что такое внедрение зависимостей (dependency injection, DI)
Ранее мы поняли, что такое зависимость и как она влияет на проект. Сейчас разберем что такое внедрение зависимостей.
Перед тем, как разбирать внедрение зависимостей, нам нужно понять как избежать ловушки, в которой мы будем окружены зависимостями. Проблема сильных связей (hard dependency) как Белые Ходоки (White Walkers). Мы не должны присоединиться к их армии, напротив, нужно найти путь, чтобы победить их.
Стратегия решения проблемы сильных связей (hard dependency) или проблемы Белых Ходоков
Нам нужен четкий план для решения или предотвращения проблемы сильных связей. Эта стратегия или план называется внедрение зависимостей (dependency injection, DI). Другими словами, чтобы убить Белого Ходока вам нужно сжечь его или использовать оружие из драконьего стекла. Аналогично, чтобы избежать сильных связей, необходимо использовать внедрение зависимостей.
Внедрение зависимостей — это лишь один из методов предотвращения сильных связей. Существует много других способов. Для Android разработки этот метод считается наиболее простым и многие крупномасштабные проекты используют стратегию внедрения зависимостей.
Метод внедрения зависимостей
Внедрение зависимостей — это метод, при котором один объект предоставляет зависимости другого объекта. Зависимость (dependency) — это объект, который мы можем использовать (сервис). Внедрение (инъекция, injection) — это передача зависимости зависимому объекту (клиенту), который будет данной зависимостью пользоваться. Сервис — это часть состояния клиента. Передать сервис клиенту, вместо того, чтобы позволить клиенту создать или найти сервис — базовое требование шаблона проектирования «Внедрение зависимости» (DI).
Сравним это с Игрой Престолов. Серсея готовится к большой войне. Вместо того, чтобы сжигать все деньги в своем королевстве она пытается получить кредит у железного банка Браавоса. В этой ситуации деньги — это зависимость. Деньги нужны всем домам, чтобы вести войну. Таким образом внешний объект (железный банк) будет внедрять зависимость (деньги) зависимым объектам (домам).
Другими словами, внедрение зависимостей основывается на концепции инверсии контроля, которая говорит о том, что класс должен получать свои зависимости извне. Говоря просто, ни один класс не должен создавать экземпляр другого класса, а должен получать все экземпляры из класса конфигурации.
Пример 1
Хватить говорить. Давайте разбирать код. Рассмотрим небольшой пример с двумя сценариями. В первом сценарии создадим несколько классов с сильными связями (hard dependencies), то есть без использования внедрения зависимостей. Затем, следуя шаблону «Внедрение зависимости», мы избавимся от сильных связей.
Сценарий 1. Без использования внедрения зависимостей
Проблема: Битва бастардов — Старки (Starks) и Болтоны (Boltons) готовятся к войне, чтобы захватить Север. Нужно их подготовить и вывести на войну.
Поскольку пример может включать много домов, создадим общий интерфейс House , включим в него методы prepareForWar() и reportForWar() .
Далее создадим классы для домов Starks и Boltons . Классы будут реализовать интерфейс House .
Заметка: this.getClass().getSimpleName() просто возвращает имя класса
Далее нужно привести оба дома к войне. Создадим класс War и попросим оба дома подготовиться к войне и сообщить о ней.
Анализируем War
Рассмотрим конструктор класса War . Для работы ему необходимы два класса Starks и Boltons . Эти классы создаются внутри конструктора, готовятся к войне и сообщают о ней.
В хорошо спроектированном объектно-ориентированном приложении у каждого объекта минимальное количество обязанностей, объекты опираются на другие для выполнения большей части работы. В данном примере класс War зависит от Starks и Boltons . Это зависимости класса War . Без них War работать не будет. Перед тем, как класс начнет выполнять реальные функции, все его зависимости должны быть удовлетворены каким-либо образом. Зависимости класса War в этом примере удовлетворяются путем создания экземпляров классов в конструкторе.
Хотя вариант с созданием зависимостей внутри конструктора класса достаточно хорошо работает в небольших приложениях — он привносит недостатки по мере роста проекта.
Во-первых, класс становится довольно негибким. Если приложение должно работать на нескольких платформах или в нескольких режимах, например, необходимо заменить класс Boltons на другой или разделить его на несколько объектов. Сделать это становится не так просто.
Во-вторых, невозможно изолированно протестировать эти классы. Создание экземпляра War автоматически создает два других объекта, которые, в конечном итоге, будут тестироваться вместе с классом War . Это может стать проблемой, если один из объектов зависит от дорогого внешнего ресурса, например, Allies (союзники) или один из объектов сам зависит от множества других.
Как упоминалось ранее — борьба с сильными связями (hard dependencies) похожа на борьбу с Белыми Ходоками без надлежащего оружия.
Резюме
В этой статье мы рассмотрели зависимости и внедрение зависимостей в деталях. Ни один класс не должен создавать экземпляр другого класса, а должен получать все экземпляры из класса конфигурации.
Внедрение зависимостей — это метод решения или предотвращения проблемы сильных связей. Предоставляя зависимости извне, этот метод делает класс независимым, тестируемым и поддерживаемым.
Также мы рассмотрели пример с сильными связями (hard dependencies), создавая сценарий битвы бастардов.
Источник
Dagger & Android
One of the primary advantages of Dagger 2 over most other dependency injection frameworks is that its strictly generated implementation (no reflection) means that it can be used in Android applications. However, there are still some considerations to be made when using Dagger within Android applications.
Philosophy
While code written for Android is Java source, it is often quite different in terms of style. Typically, such differences exist to accomodate the unique performance considerations of a mobile platform.
But many of the patterns commonly applied to code intended for Android are contrary to those applied to other Java code. Even much of the advice in Effective Java is considered inappropriate for Android.
In order to achieve the goals of both idiomatic and portable code, Dagger relies on ProGuard to post-process the compiled bytecode. This allows Dagger to emit source that looks and feels natural on both the server and Android, while using the different toolchains to produce bytecode that executes efficiently in both environments. Moreover, Dagger has an explicit goal to ensure that the Java source that it generates is consistently compatible with ProGuard optimizations.
Of course, not all issues can be addressed in that manner, but it is the primary mechanism by which Android-specific compatibility will be provided.
Dagger assumes that users on Android will use R8 or ProGuard.
Why Dagger on Android is hard
One of the central difficulties of writing an Android application using Dagger is that many Android framework classes are instantiated by the OS itself, like Activity and Fragment , but Dagger works best if it can create all the injected objects. Instead, you have to perform members injection in a lifecycle method. This means many classes end up looking like:
This has a few problems:
Copy-pasting code makes it hard to refactor later on. As more and more developers copy-paste that block, fewer will know what it actually does.
More fundamentally, it requires the type requesting injection ( FrombulationActivity ) to know about its injector. Even if this is done through interfaces instead of concrete types, it breaks a core principle of dependency injection: a class shouldn’t know anything about how it is injected.
dagger.android
The classes in dagger.android offer one approach to simplify the above problems. This requires learning some extra APIs and concepts but gives you reduced boilerplate and injection in your Android classes at the right place in the lifecycle.
Another approach is to just use the normal Dagger APIs and follow guides such as the one here. This may be simpler to understand but comes with the downside of having to write extra boilerplate manually.
The Jetpack and Dagger teams are working together on a new initiative for Dagger on Android that hopes to be a large shift from the current status quo. While it is unfortunately not ready yet, this may be something to consider when choosing how to use Dagger in your Android projects today.
Injecting Activity objects
Install AndroidInjectionModule in your application component to ensure that all bindings necessary for these base types are available.
Start off by writing a @Subcomponent that implements AndroidInjector , with a @Subcomponent.Factory that extends AndroidInjector.Factory :
After defining the subcomponent, add it to your component hierarchy by defining a module that binds the subcomponent factory and adding it to the component that injects your Application :
Pro-tip: If your subcomponent and its factory have no other methods or supertypes other than the ones mentioned in step #2, you can use @ContributesAndroidInjector to generate them for you. Instead of steps 2 and 3, add an abstract module method that returns your activity, annotate it with @ContributesAndroidInjector , and specify the modules you want to install into the subcomponent. If the subcomponent needs scopes, apply the scope annotations to the method as well.
Next, make your Application implement HasAndroidInjector and @Inject a DispatchingAndroidInjector to return from the androidInjector() method:
Finally, in your Activity.onCreate() method, call AndroidInjection.inject(this) before calling super.onCreate(); :
How did that work?
AndroidInjection.inject() gets a DispatchingAndroidInjector from the Application and passes your activity to inject(Activity) . The DispatchingAndroidInjector looks up the AndroidInjector.Factory for your activity’s class (which is YourActivitySubcomponent.Factory ), creates the AndroidInjector (which is YourActivitySubcomponent ), and passes your activity to inject(YourActivity) .
Injecting Fragment objects
Injecting a Fragment is just as simple as injecting an Activity . Define your subcomponent in the same way.
Instead of injecting in onCreate() as is done for Activity types, inject Fragment s to in onAttach() .
Unlike the modules defined for Activity s, you have a choice of where to install modules for Fragment s. You can make your Fragment component a subcomponent of another Fragment component, an Activity component, or the Application component — it all depends on which other bindings your Fragment requires. After deciding on the component location, make the corresponding type implement HasAndroidInjector (if it doesn’t already). For example, if your Fragment needs bindings from YourActivitySubcomponent , your code will look something like this:
Base Framework Types
Because DispatchingAndroidInjector looks up the appropriate AndroidInjector.Factory by the class at runtime, a base class can implement HasAndroidInjector as well as call AndroidInjection.inject() . All each subclass needs to do is bind a corresponding @Subcomponent . Dagger provides a few base types that do this, such as DaggerActivity and DaggerFragment , if you don’t have a complicated class hierarchy. Dagger also provides a DaggerApplication for the same purpose — all you need to do is to extend it and override the applicationInjector() method to return the component that should inject the Application .
The following types are also included:
Note: DaggerBroadcastReceiver should only be used when the BroadcastReceiver is registered in the AndroidManifest.xml . When the BroadcastReceiver is created in your own code, prefer constructor injection instead.
Support libraries
For users of the Android support library, parallel types exist in the dagger.android.support package.
TODO(ronshapiro): we should begin to split this up by androidx packages
Источник
Dagger 2 и структура приложения для Android
Добрый день! Наша команда уже больше года занимается разработкой почтового клиента МойОфис для платформы Android (приложения МойОфис мы разрабатываем для всех популярных платформ).
Сегодня мы хотим рассказать о технологиях, которые мы используем в разработке нашего почтового клиента. А именно, о механизмах Dependency Injection в виде библиотеки Dagger 2. В статье мы опишем основные части библиотеки и расскажем, как их использовать в Android-проекте.
Почему Dagger 2
До того как начать применять Dagger 2, мы не использовали паттерн Dependency Injection (DI). Это похоже на то, как добавить слишком много крупы в кашу: наш код был слишком связанный и это мешало свободному тестированию и редактированию кода.
В этот же период Google анонсировал библиотеку Dagger 2 – это был самый новый вариант. Мы сравнили имеющиеся аналоги и для почтового клиента МойОфис остановились именно на ней.
Библиотека Dagger 2 обладает рядом достоинств перед другими Dependency Injection библиотеками. Её принципиальное достоинство – работа на принципе кодогенерации, без рефлексии. Из этого следует, что любые ошибки, связанные с построением графа зависимостей, обнаружатся ещё в момент компиляции проекта.
Внедрением DI в наш проект мы смогли красиво избавиться от сильной связанности между различными модулями нашего приложения. Также мы смогли убрать большинство синглтонов, использование которых было неоправданно. Уже сейчас мы видим, как повысилась эффективность написания и редактирования кода. В дальнейшем у нас будет возможность упростить задачу покрытия проекта Unit и UI тестами, что в свою очередь приведёт к повышению стабильности работы приложения.
В этой статье мы хотим предоставить полный обзор Dagger 2.
Мы рассмотрим основные части Dagger 2:
- варианты запроса зависимости;
- модули, предоставляющие объекты для внедрения;
- компоненты, связующие запросы с объектами для внедрения;
и расскажем, как использовать дополнительные части Dagger 2:
- отложенная и асинхронная инициализация зависимостей.
Виды @ Inject
Существует несколько способов запроса зависимости:
1) Внедрение в конструктор класса. Бонусом такого варианта служит неявная доступность использования этой зависимости для внедрения (ManagerA не обязательно прописывать в модуле). Если конструктор имеет параметры, необходимо, чтобы они находились в графе зависимостей и могли быть внедрены.
2) Внедрение через метод. Метод будет выполнен после вызова конструктора.
3) Внедрение в поля класса. Поля должны быть не приватными и не финальными.
4) Вызов геттера необходимого нам объекта. Этот геттер также используется для связывания нескольких графов зависимостей.
@ Module
Модуль – это фабрика объектов, разрешающая наши зависимости. Он должен быть помечен аннотацией @ Module, а методы, генерирующие зависимости, – @ Provides. И если необходимо отметить область видимости, то модуль помечаем одной из аннотаций @ Scope.
Аннотация @ Module может содержать в себе другие модули.
Таким образом зависимости, содержащиеся в них, будут доступны в ссылающемся на них модуле.
Модуль может содержать конструктор с параметром, если для разрешения зависимостей ему нужны данные извне. Наличие конструктора вносит важное отличие в создание компонента, о чём будет сказано ниже.
Также могут иметь место каскадные зависимости:
@ Component
Компонент является связующим звеном между модулями и просителями зависимостей. Отдать зависимость можно через метод компонента (в который будет передан объект, запрашивающий зависимости) или через геттер (который вернёт зависимость). В одном компоненте могут быть как методы, так и геттеры. Названия методов или геттеров не важны.
В обоих случаях мы сначала создаём интерфейс и помечаем его аннотацией @ Component или @ Subcomponent. Далее указываем, как будут разрешаться зависимости. Необходимо добавить список модулей, которые будут генерировать зависимости.
В случае внедрения через метод список необходимых зависимостей берётся из самого класса и его базовых классов:
Компонент, содержащий как метод, так и геттер, будет выглядеть так:
Дальше нужно собрать проект. Будут сгенерированы классы вида DaggerНазваниеВашегоКомпонента, являющиеся наследниками вашего компонента. Для создания экземпляра компонента воспользуемся билдером. В зависимости от того, имеет ли модуль конструктор с параметрами или нет, мы можем действовать по-разному.
Если есть параметризованный конструктор модуля, то нужно задать все такие модули собственноручно:
Если нет, то вдобавок к билдеру будет сгенерирован метод create() и изменён метод build():
@ Scope
Рассмотрим Android и применение скоупов. Аннотацией @ Scope и её наследниками помечаются методы в модулях, которые генерируют объекты для внедрения. Если Produce-метод помечен скоупом, то и любой компонент, использующий этот модуль, должен быть помечен этим же скоупом.
Разные менеджеры имеют разные области видимости. Например, DataBaseHelper должен быть один для всего приложения. Для этого обычно использовали синглтон. В Dagger есть такой скоуп @ Singletone, которым помечают объекты, необходимые в одном экземпляре для всего приложения. Но мы решили использовать свой скоуп @ PerApplication для полной аналогии названий со скоупами активити и фрагмента.
Название скоупа не имеет значения – важен уровень вложенности компонентов и их скоупов.
Уровень приложения
Аннотации, определяющие области видимости, объявляются так:
Используется это так:
В рамках одного модуля и тех, которые прописаны у него в includes, должен использоваться один и тот же скоуп, иначе во время компиляции вы получите ошибку построения графа зависимостей.
Теперь мы должны пометить компоненты, использующие этот модуль:
Тут стоить обратить внимание, что DI удобно использовать для тестов, и нам хотелось бы иметь возможность подменить db на его имитацию. Для этого желательно вынести DbHelper в отдельный модуль:
Как вы можете заметить, этот модуль не содержит context и не способен самостоятельно его разрешить. Сontext берётся из ссылающегося на него модуля:
Уровень Activity
Объектов Activity в приложении может быть несколько, и их компоненты нужно связать с компонентом Application. Рассмотрим параметры аннотаций @ Component и @ Subcomponent и их участие в построении графа зависимостей.
Предположим, у нас есть менеджер EventBus для общения между Activity и фрагментом. Его область видимости – это один экземпляр менеджера на Activity и фрагменты, которые находятся в Activity.
Но во время компиляции нам сразу говорят, что ActComponent не может внедрить зависимость DbHelper. Волшебства, конечно, не произошло. У нас получилось два разных несвязанных графа зависимостей. И второй граф не знает, откуда брать DbHelper.
У нас есть два варианта: либо связать компоненты через интерфейс, который будет предоставлять нам все необходимые зависимости, либо, используя первый компонент, создать второй, тогда граф получится один.
В аннотации @Component есть параметр dependencies, который указывает на список интерфейсов компонентов, предоставляющий необходимые зависимости.
В этом случае добавляем в AppComponent геттер зависимости.
Для второго способа нужно пометить наш внутренний компонент аннотацией @ Subcomponent. Кроме списка модулей, у неё нет других параметров.
А в AppComponent добавляем метод, возвращающий ActComponent. Есть общее правило, которое заключается в том, что если Subcomponent имеет модуль с параметризованным конструктором, его нужно обязательно передать в наш метод. Иначе в момент создания компонента произойдёт ошибка.
Недостатком варианта с SubComponent является то, что если ActComponent или ActModule будут содержать в себе несколько других модулей, то потребуется увеличивать количество параметров метода Plus для возможности передачи изменённого модуля:
Итого: вариант с компонентом и dependencies выглядит более гибким, но будет необходимо описать все нужные зависимости в интерфейсе.
Уровень Fragment
Внедрение зависимостей во фрагменты интереснее, так как фрагмент может быть использован в нескольких Activity. Например, приложение со списком объектов и их детальным описанием, когда на телефоне используются две Activity, а на планшете – одна Activity с двумя фрагментами.
Для нашего почтового клиента мы решили использовать под каждый фрагмент свой компонент, даже если нужно внедрить только одну зависимость. Это облегчит нашу работу, если потребуется обновлять список зависимостей во фрагменте. Здесь также есть два варианта создания компонента:
Используем @ Component и его параметр dependencies
Сразу видим проблему: наш компонент зависит от конкретного компонента Activity. Подходящее решение – когда для каждого компонента фрагмента создаётся интерфейс, описывающий необходимые для него зависимости:
Теперь перейдём к применению. Нам нужно вытащить компонент из Activity вне зависимости от конкретной Activity. Для этого используем:
Итого наследуем наши Activity от него:
И теперь можем использовать вместо конкретной Activity этот интерфейс:
2) Используем @ Subcomponent и метод plus для его создания:
Чтобы избежать дублирования кода, выносим cамые общие зависимости и общий код в базовые Activity и фрагмент:
Теперь инициализация компонента во фрагменте выглядит так:
Lazy‹T› и Provider ‹T›
Допустим, у нас есть менеджер, который инициализируется длительное время. Не хотелось бы, чтобы при запуске приложения все такие зависимости разом занимали главный поток. Нужно отложить внедрение этих зависимостей до момента их использования. Для этого в Dagger 2 есть интерфейсы Lazy и Provider, реализующие отложенную инициализацию зависимостей.
Если ManagerA имеет некий скоуп, то их поведение идентично, но если скоуп отсутствует, Lazy после инициализации кеширует зависимость, а Provider генерирует каждый раз новую.
Producer
Также сейчас ведутся разработки асинхронной инициализации зависимостей. Для того чтобы посмотреть на них, нужно добавить:
compile ‘com.google.dagger:dagger-producers:2.0-beta’
И небольшой пример:
Получаем ListenableFuture, c которым уже можем работать, например, обернуть в Rx Observable. Готово!
Ниже приведены ссылки на проект с примерами и полезными презентациями:
В следующих статьях мы готовы рассказать о наших мобильных разработках и об используемых технологиях. Спасибо за внимание и с наступающим Новым годом!
Источник