- 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. Продвинутый уровень. Часть 1
- Серия статей
- Ранее в цикле статей
- Предисловие
- Дом Android
- Описание проекта
- #Классы и пакеты
- Зависимости
- Существующие проблемы
- #Неуклюжая инициализация объектов
- #Тестируемость
- Немного усложним пример
- Граф зависимостей
- Внедряем зависимости с помощью Dagger 2
- Заметка:
- Шаг 1. Установка Dagger 2
- Шаг 2. Создание Component
- Шаг 3. Создание модулей
- Шаг 4. Соединяем модули
- Шаг 5. Связывание Component и модулей
- Шаг 6. Сборка проекта
- Шаг 7. Поздравьте себя!
- Но есть проблема
- Аннотация @Scope
- Использование областей (Scope)
- Ещё одна проблема!
- Аннотация @Named
- Альтернатива аннотации @Named — @Qualifier
- Резюме
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 разработчиков. Dagger 2. Продвинутый уровень. Часть 1
Данная статья является шестой частью серии статей, предназначенных, по словам автора, для тех, кто не может разобраться с внедрением зависимостей и фреймворком Dagger 2, либо только собирается это сделать. Оригинал написан 23 декабря 2017 года. Перевод вольный.
Это шестая статья цикла «Dagger 2 для начинающих Android разработчиков.». Если вы не читали предыдущие, то вам сюда.
Серия статей
Ранее в цикле статей
Мы проанализировали генерируемый Dagger 2 класс и посмотрели на то, как Dagger 2 использует шаблон Builder для предоставления необходимых зависимостей.
После разобрали простой пример использования аннотаций @Module и @Provides .
Предисловие
Эта статья может показаться вам немного большой. Обычно мои статьи не превышают 800 символов. Я хотел разбить её на более мелкие части, но причина, по которой статья настолько большая, заключается в том, что если при решении проблемы сильных связей (hard dependencies) надолго остановиться посередине, то возникает шанс потеряться.
Но я включил в статью контрольные точки (Сheckpoint). В этих местах вы можете взять небольшой перерыв и отвлечься. Думаю, это будет полезным для новичков в Dagger 2 и внедрении зависимостей (DI).
Дом Android
До сих пор мы рассматривали обычные Java проекты в примерах. Я надеюсь, что у большинства из вас теперь есть представление о DI и том как Dagger 2 позволяет реализовать DI. Теперь же погрузимся в реальный пример Android приложения и попробуем использовать в этом проекте Dagger 2.
Чтобы собрать всё в одном месте, как в Google code labs, я создал ветку kickstart. Нашей целью будет устранение сильных связей в этом проекте. Части решения будут находится в отдельных ветках этого проекта.
Описание проекта
Это очень простой проект. В нем мы будем получать случайных пользователей, используя Random Users API и показывать их в RecyclerView . Я не буду тратить много времени на объяснение проекта, буду объяснять абстрактно. Но, пожалуйста, разбирайте код внимательно, чтобы внедрение Dagger 2 в проект проходило для вас максимально понятно и просто.
#Классы и пакеты
Зависимости
Для реализации функций проекта будут задействованы следующие библиотеки:
- Retrofit — для вызовов API
- GsonBuilder и Gson — для работы с JSON
- HttpLoggingInterceptor — для логирования сетевых операций
- OkHttpClient — клиент для Retrofit
- Picasso — работа с изображениями в Adapter
Как мы видели в предыдущих примерах, в MainActivity присутствуют зависимости. И каждый раз при создании MainActivity экземпляры зависимостей будут создаваться снова и снова.
Существующие проблемы
Если посмотрите на MainActivity , то заметите следующие проблемы:
#Неуклюжая инициализация объектов
Когда вы смотрите на метод onCreate() , то можете найти неуклюжими инициализации внутри него. Мы, конечно, можем продолжать таким образом инициализировать объекты в этом методе, но лучше отыскать верный путь для решения проблемы.
#Тестируемость
Также нужно найти путь для того, чтобы тестировать наш код. И Picasso внутри Adapter тоже мешает возможности тестирования. Было бы неплохо передавать эту зависимость через конструктор.
Немного усложним пример
Зависимости, представленные выше в классе MainActivity , нужны были лишь для того, чтобы вы немного вникли в проект и почувствовали себя комфортно. Если углубиться, то как в любом реальном проекте зависимостей станет больше. Давайте добавим ещё несколько.
Кроме ранее рассмотренных зависимостей добавим следующие:
- File — для хранения кэша
- Cache — для сетевого кеша
- OkHttp3Downloader — загрузчик, использующий OkHttpClient для загрузки изображений
- Picasso — для обработки изображений из сети
Код будет выглядеть следующим образом (полный пример вы можете просмотреть в отдельной ветке):
Граф зависимостей
Граф зависимостей — это не что иное, как диаграмма, объясняющая зависимости между классами. Формирование такого графа делает реализацию более понятной (вы убедитесь в этом ближе к концу). Посмотрите на граф зависимостей для нашего проекта.
Зеленым отмечены верхнеуровневые зависимости, это означает, что они не нужны никаким другим зависимостям, но им нужны некоторые из зависимостей.
Как читать эту диаграмму? Например, у Picasso две зависимости — OkHttp3Downloader и Context .
Для получения случайных пользователей с помощью API вам нужен Retrofit . Ему, в свою очередь, нужны две зависимости — GsonConvertFactory и OkHttpClient и так далее.
Уделите время на то, чтобы посмотреть на код в MainActivity и сравнить его с диаграммой для лучшего понимания.
(Checkpoint)
.
Внедряем зависимости с помощью Dagger 2
Полный код вы можете найти в отдельной ветке проекта.
Заметка:
Шаг 1. Установка Dagger 2
Просто добавьте несколько строк в build.gradle файл.
Шаг 2. Создание Component
Component будет интерфейсом для всего графа зависимостей. Лучшей практикой использования Component является объявление только верхнеуровневых зависимостей в нём и скрытие остальных зависимостей.
Это означает, что в Component будут присутствовать только те зависимости, которые помечены в графе зависимостей зеленым цветом, то есть RandomUsersAPI и Picasso .
Как сам Component поймет где взять зависимости RandomUsersAPI и Picasso ? Воспользуемся модулями.
Шаг 3. Создание модулей
Сейчас нужно переместить код из MainActivity в различные модули. Смотря на граф зависимостей можно решить какие модули необходимы.
Первый — RandomUsersModule , он предоставит зависимости RandomUsersApi , GsonConverterFactory , Gson и Retrofit .
Второй — PicassoModule , который предоставит зависимости Picasso и OkHttp3Downloader .
В модуле RandomUsersModule для Retrofit нужен OkHttpClient . Которому, в свою очередь, нужны другие зависимости. Почему бы не сделать для этого отдельный модуль?
Создадим OkHttpClientModule , который предоставит OkHttpClient , Cache , HttpLoggingInterceptor и File .
Модули почти готовы, но PicassoModule и OkHttpClientModule требуется Context и, возможно, он пригодится нам в других местах. Сделаем модуль и для этих целей.
Шаг 4. Соединяем модули
Сейчас у нас есть все модули и компонент, как на картинке ниже. Но как передать Context в другие модули? Нам нужно связать модули, которые зависят друг от друга.
Для реализации связи между модулями требуется атрибут includes . Этот атрибут включает в текущий модуль зависимости модулей, на которые указана ссылка.
Какие модули нужно связать?
- RandomUsersModule нуждается в OkHttpClientModule
- OkHttpClientModule нуждается в ContextModule
- PicassoModule нуждается в OkHttpClientModule и ContextModule . Так как OkHttpClientModule уже связан с ContextModule , то можно обойтись только OkHttpClientModule
Таким образом мы соединили все модули.
Шаг 5. Связывание Component и модулей
На данный момент все модули соединены и общаются друг с другом. Сейчас время сказать Component или обучить его обращаться к модулям, которые предоставят ему требуемые зависимости.
Как мы связывали модули между собой с помощью атрибута includes , подобным образом мы можем связать компонент и модули с помощью атрибута modules .
Учитывая потребности созданного компонента (методы getRandomUserService() и getPicasso() ) включим в компонент ссылки на модули RandomUsersModule и PicassoModule , используя атрибут modules .
Шаг 6. Сборка проекта
Если вы всё сделали верно, то Dagger 2 сгенерирует на основе созданного нами компонента класс, предоставляющий нужные зависимости.
Теперь в MainActivity можно удобно получить зависимости Picasso и RandomUsersApi с помощью RandomUserComponent .
Шаг 7. Поздравьте себя!
Но есть проблема
Что? Какая проблема?
Каждый раз, когда вы вызываете DaggerComponent.build() создаются новые экземпляры всех объектов или зависимостей, которые вы настроили. В этом случае почему Dagger 2 не знает о том, что мне нужен только один экземпляр Picasso ? Другими словами, как мы можем сказать Dagger 2 предоставлять нам зависимость как singleton?
Аннотация @Scope
Аннотация @Scope говорит Dagger 2 создавать только единственный экземпляр, даже если DaggerComponent.build() вызывается многократно. Это заставляет зависимость работать как singleton. Для настройки требуемой области (Scope) необходимо создать собственную аннотацию.
@Retention — это аннотация для обозначения точки отклонения использования аннотации. Она говорит о том, когда может быть использована аннотация. Например, с отметкой SOURCE аннотация будет доступна только в исходном коде и будет отброшена во время компиляции, с отметкой CLASS аннотация будет доступна во время компиляции, но не во время работы программы, с отметкой RUNTIME аннотация будет доступна и во время выполнения программы.
Использование областей (Scope)
Для использования созданной области нужно начать с компонента, отметив созданной аннотацией, а затем отметить каждый метод, который нам нужен как singleton.
Вот как мы создаем единственный экземпляр.
Ещё одна проблема!
Как правило, в каждом приложении мы используем два вида контекста — ApplicationContext и контекст Activity . Как их предоставить? Для предоставления ApplicationContext можно использовать ContextModule . Давайте создадим ещё один модуль.
Но созданный класс не решает проблемы. Теперь мы предоставляем две зависимости с типом Context и Dagger 2 не сможет понять каким воспользоваться, возникнет ошибка.
Аннотация @Named
Эта аннотация поможет нам различить контекст. Просто добавим эту аннотацию к методам, не забыв про атрибут.
Далее укажем Dagger 2 использовать соответствующий контекст в нужных местах.
Альтернатива аннотации @Named — @Qualifier
Для замены аннотации @Named на @Qualifier нужно создать отдельную аннотацию и использовать её где необходимо.
Затем пометим аннотацией метод, предоставляющий соответствующую зависимость.
Далее отметим параметры всех методов, где нам необходим ApplicationContext созданной аннотацией.
Взгляните на соответствующий commit для того, чтобы увидеть как можно заменить аннотацию @Named на @Qualifier .
Резюме
На данный момент мы взяли простой проект и внедрили зависимости в нем с помощью Dagger 2 и аннотаций.
Также мы изучили 3 новые аннотации. Первая — @Scope , для получения зависимостей в единственном экземпляре. Вторая — @Named , для разделения методов, предоставляющих зависимости одинакового типа. Третья — @Qualifier , как альтернатива @Named .
Источник