Dagger 2 и структура приложения для Android
Добрый день! Наша команда уже больше года занимается разработкой почтового клиента МойОфис для платформы Android (приложения МойОфис мы разрабатываем для всех популярных платформ).
Сегодня мы хотим рассказать о технологиях, которые мы используем в разработке нашего почтового клиента. А именно, о механизмах Dependency Injection в виде библиотеки Dagger 2. В статье мы опишем основные части библиотеки и расскажем, как их использовать в Android-проекте.
До того как начать применять Dagger 2, мы не использовали паттерн Dependency Injection (DI). Это похоже на то, как добавить слишком много крупы в кашу: наш код был слишком связанный и это мешало свободному тестированию и редактированию кода.
В этот же период Google анонсировал библиотеку Dagger 2 — это был самый новый вариант. Мы сравнили имеющиеся аналоги и для почтового клиента МойОфис остановились именно на ней.
Библиотека Dagger 2 обладает рядом достоинств перед другими Dependency Injection библиотеками. Её принципиальное достоинство — работа на принципе кодогенерации, без рефлексии. Из этого следует, что любые ошибки, связанные с построением графа зависимостей, обнаружатся ещё в момент компиляции проекта.
Внедрением DI в наш проект мы смогли красиво избавиться от сильной связанности между различными модулями нашего приложения. Также мы смогли убрать большинство синглтонов, использование которых было неоправданно. Уже сейчас мы видим, как повысилась эффективность написания и редактирования кода. В дальнейшем у нас будет возможность упростить задачу покрытия проекта Unit и UI тестами, что в свою очередь приведёт к повышению стабильности работы приложения.
В этой статье мы хотим предоставить полный обзор Dagger 2.
Мы рассмотрим основные части Dagger 2:
- варианты запроса зависимости;
- модули, предоставляющие объекты для внедрения;
- компоненты, связующие запросы с объектами для внедрения;
и расскажем, как использовать дополнительные части Dagger 2:
- отложенная и асинхронная инициализация зависимостей.
Существует несколько способов запроса зависимости:
1) Внедрение в конструктор класса. Бонусом такого варианта служит неявная доступность использования этой зависимости для внедрения (ManagerA не обязательно прописывать в модуле). Если конструктор имеет параметры, необходимо, чтобы они находились в графе зависимостей и могли быть внедрены.
//тут можно поместить @Scope зависимости
public class ManagerA{
@Inject
public ManagerA(Context context){ /* */}
}
2) Внедрение через метод. Метод будет выполнен после вызова конструктора.
@Inject
public void register(SomeDepends depends){
depends.register(this);
}
3) Внедрение в поля класса. Поля должны быть не приватными и не финальными.
@Inject ManagerB managerB;
4) Вызов геттера необходимого нам объекта. Этот геттер также используется для связывания нескольких графов зависимостей.
managerA = component.getManagerA();
Модуль — это фабрика объектов, разрешающая наши зависимости. Он должен быть помечен аннотацией @ Module, а методы, генерирующие зависимости, — @ Provides. И если необходимо отметить область видимости, то модуль помечаем одной из аннотаций @ Scope.
Аннотация @ Module может содержать в себе другие модули.
@Module(includes={SomeModule1.class, SomeModule2.class})
Таким образом зависимости, содержащиеся в них, будут доступны в ссылающемся на них модуле.
Модуль может содержать конструктор с параметром, если для разрешения зависимостей ему нужны данные извне. Наличие конструктора вносит важное отличие в создание компонента, о чём будет сказано ниже.
@Module
AppModule{
Application app;
AppModule(App app){
this.app = app;
}
@PerApplication
@Provides
Context provideContext(){return app;}
}
Также могут иметь место каскадные зависимости:
@Provides
RestAdapter provideRestAdapter(){return new RestAdapter()}
@Provides
GitHubApi provideRetrofitAdapter(RestAdapter adapter){
return adapter.create(GitHubApi.class)
}
Компонент является связующим звеном между модулями и просителями зависимостей. Отдать зависимость можно через метод компонента (в который будет передан объект, запрашивающий зависимости) или через геттер (который вернёт зависимость). В одном компоненте могут быть как методы, так и геттеры. Названия методов или геттеров не важны.
В обоих случаях мы сначала создаём интерфейс и помечаем его аннотацией @ Component или @ Subcomponent. Далее указываем, как будут разрешаться зависимости. Необходимо добавить список модулей, которые будут генерировать зависимости.
В случае внедрения через метод список необходимых зависимостей берётся из самого класса и его базовых классов:
class App{
@Inject
ManagerA managerA;
}
Компонент, содержащий как метод, так и геттер, будет выглядеть так:
@Component(modules={AppModule.class})
interface AppComponent{
void injectInto(App holder);
ManagerA getManagerA ();
}
Дальше нужно собрать проект. Будут сгенерированы классы вида DaggerНазваниеВашегоКомпонента, являющиеся наследниками вашего компонента. Для создания экземпляра компонента воспользуемся билдером. В зависимости от того, имеет ли модуль конструктор с параметрами или нет, мы можем действовать по-разному.
Если есть параметризованный конструктор модуля, то нужно задать все такие модули собственноручно:
AppModule module = new AppModule(this);
DaggerAppComponent.builder().appModule(module).build();
//сгенерированный код
public SecondActComponent build() {
if (appModule == null) {
throw new IllegalStateException("appModule must be set");
}
return new DaggerAppComponent (this);
}
Если нет, то вдобавок к билдеру будет сгенерирован метод create () и изменён метод build ():
DaggerAppComponent.create();
//сгенерированный код
public static AppComponent create() {
return builder().build();
}
//сгенерированный код
public AppComponent build() {
if (appModule == null) {
this.appModule = new appModule();
}
return new DaggerAppComponent (this);
}
class App{
@Inject
ManagerA managerA;
AppComponent component
@Override
onCreate(){
//… инициализация компонента
component.inject(this);
//или
managerA= component.getmanagerA();
super.onCreate()
}
}
Рассмотрим Android и применение скоупов. Аннотацией @ Scope и её наследниками помечаются методы в модулях, которые генерируют объекты для внедрения. Если Produce-метод помечен скоупом, то и любой компонент, использующий этот модуль, должен быть помечен этим же скоупом.
Разные менеджеры имеют разные области видимости. Например, DataBaseHelper должен быть один для всего приложения. Для этого обычно использовали синглтон. В Dagger есть такой скоуп @ Singletone, которым помечают объекты, необходимые в одном экземпляре для всего приложения. Но мы решили использовать свой скоуп @ PerApplication для полной аналогии названий со скоупами активити и фрагмента.
Название скоупа не имеет значения — важен уровень вложенности компонентов и их скоупов.
Уровень приложения
Аннотации, определяющие области видимости, объявляются так:
@Scope
@Retention(RUNTIME)
@interface PerApplication;
Используется это так:
AppModule{
//...
@Provides
@PerApplication
dbHelper provideDbHelper(Context context){
return new DbHelper(context)}
@Provides
@PerApplication
context provideContext(){
return app}
}
В рамках одного модуля и тех, которые прописаны у него в includes, должен использоваться один и тот же скоуп, иначе во время компиляции вы получите ошибку построения графа зависимостей.
Теперь мы должны пометить компоненты, использующие этот модуль:
@PerApplication
@Component(modules={AppModule.class}){
void inject(App);
}
class App extends Application{
@Inject
DbHelper dbHelper;
Appcomponent comp;
@Override
onCreate(){
super.onCreate();
comp = DaggerAppComponent.builder()
.appModule(new AppModule(this))
.build();
}
Тут стоить обратить внимание, что DI удобно использовать для тестов, и нам хотелось бы иметь возможность подменить db на его имитацию. Для этого желательно вынести DbHelper в отдельный модуль:
@Module
DBModule{
@PerApp
@Provides
DbHelper dbhelper(Context context){
return new DbHelper(context)}
}
Как вы можете заметить, этот модуль не содержит context и не способен самостоятельно его разрешить. Сontext берётся из ссылающегося на него модуля:
@Module(Includes={DbModule.class})
И теперь:
Уровень Activity
Объектов Activity в приложении может быть несколько, и их компоненты нужно связать с компонентом Application. Рассмотрим параметры аннотаций @ Component и @ Subcomponent и их участие в построении графа зависимостей.
Предположим, у нас есть менеджер EventBus для общения между Activity и фрагментом. Его область видимости — это один экземпляр менеджера на Activity и фрагменты, которые находятся в Activity.
@Module
ActModule{
@PerActivity
@Provides
provide Bus(){return new Bus();}
@Component()
ActComponent{
inject(MainActivity activity);
class MainActivity extends Activity{
@Inject DbHelper dbHelper;
@Inject Bus bus;
}
Но во время компиляции нам сразу говорят, что ActComponent не может внедрить зависимость DbHelper. Волшебства, конечно, не произошло. У нас получилось два разных несвязанных графа зависимостей. И второй граф не знает, откуда брать DbHelper.
У нас есть два варианта: либо связать компоненты через интерфейс, который будет предоставлять нам все необходимые зависимости, либо, используя первый компонент, создать второй, тогда граф получится один.
В аннотации @Component есть параметр dependencies, который указывает на список интерфейсов компонентов, предоставляющий необходимые зависимости.
@Component(modules={ActModule.class}, dependencies={AppComponent.class})
В этом случае добавляем в AppComponent геттер зависимости.
@PerApplication
@Component(modules={AppModule.class}){
void inject(App);
DbHelper dbHelper();
}
class MainActivity extends Activity{
@Inject DbHelper dbHelper;
@Inject Bus bus;
ActComponent component;
onCreate(){
AppComp appcomp = ((App)getApp).getAppComponent();
ActMod actModule = new ActModule(this);
component= DaggerActComponent.build
.actmodule(actModule)
.appComponent(appComp)
.build();
component.inject(this);
}
Для второго способа нужно пометить наш внутренний компонент аннотацией @ Subcomponent. Кроме списка модулей, у неё нет других параметров.
@Subcomponent(modules={ActModule.class})
ActComponent{
inject(MainActivity activity);}
А в AppComponent добавляем метод, возвращающий ActComponent. Есть общее правило, которое заключается в том, что если Subcomponent имеет модуль с параметризованным конструктором, его нужно обязательно передать в наш метод. Иначе в момент создания компонента произойдёт ошибка.
@PerApp
@Component(modules={AppModule.class}){
void inject(App);
ActComponent plusActModule(ActModule module);
}
onCreate(){
AppComp appcomp = ((App)getApp).getAppComponent();
ActMod actModule = new ActModule(this);
actCom = appcomponent.plusActModule(actModule);
actCom.inject(this);
}
Недостатком варианта с SubComponent является то, что если ActComponent или ActModule будут содержать в себе несколько других модулей, то потребуется увеличивать количество параметров метода Plus для возможности передачи изменённого модуля:
ActComponent plusActModule(ActModule module, BusModule busModule/*и т.д.*/);
Итого: вариант с компонентом и dependencies выглядит более гибким, но будет необходимо описать все нужные зависимости в интерфейсе.
Уровень Fragment
Внедрение зависимостей во фрагменты интереснее, так как фрагмент может быть использован в нескольких Activity. Например, приложение со списком объектов и их детальным описанием, когда на телефоне используются две Activity, а на планшете — одна Activity с двумя фрагментами.
Для нашего почтового клиента мы решили использовать под каждый фрагмент свой компонент, даже если нужно внедрить только одну зависимость. Это облегчит нашу работу, если потребуется обновлять список зависимостей во фрагменте. Здесь также есть два варианта создания компонента:
Используем @ Component и его параметр dependencies
ActComponent{
Bus bus();
DbHelper dbhelper();
}
@Component(modules={ManagerAModule.class}, dependencies={FirstActComponent.class})
FirstFragmentComponent{
inject(FirstFragment fragment);
}
Сразу видим проблему: наш компонент зависит от конкретного компонента Activity. Подходящее решение — когда для каждого компонента фрагмента создаётся интерфейс, описывающий необходимые для него зависимости:
@Component(modules={ManagerAModule.class}, dependencies
={FirstFrComponent.HasFirstFrDepends.class})
interface FirstFragmentComponent{
void inject(FirstFragment fragment);
interface HasFirstFrDepends {
Bus bus();
DbHelper dbHelper();
}
}
@PerActivity
@Component(modules = {BusModule.class})
FirstActComponent extends FirstFrComponent.HasFirstFrDepends {
inject(FirstActivity activity)
}
Теперь перейдём к применению. Нам нужно вытащить компонент из Activity вне зависимости от конкретной Activity. Для этого используем:
interface HasComponent{
Component getComponent();
}
Итого наследуем наши Activity от него:
class FirstActivity extends Activity implements HasComponent{
FirstActComponent component;
FirstActComponent getComponent(){
return component;
}
}
И теперь можем использовать вместо конкретной Activity этот интерфейс:
class FirstFragmentextends Fragment{
FirstFrComponent component;
onActivityCreated(){
HasComponent has = (HasComponent) activity;
FirstFrComponent.HasFirstFrDepends depends = has.getComponent();
component = DaggerFirstFrComponent.builder()
.hasFirstFrDepends(actComponent)
.build();
component.inject(this);
}
}
2) Используем @ Subcomponent и метод plus для его создания:
@Subcomponent(modules = {ManagerBModule.class})
public interface SecondFrComponent {
void inject(SecondFragment fragment);
interface PlusComponent {
SecondFrComponent plusSecondFrComponent(ManagerBModule module);
}
}
Чтобы избежать дублирования кода, выносим cамые общие зависимости и общий код в базовые Activity и фрагмент:
public abstract class BaseActivity extends AppCompatActivity {
@Inject
protected Bus bus;
@Override
protected void onCreate(Bundle savedInstanceState) {
initDiComponent();
super.onCreate(savedInstanceState);
}
abstract protected void initDiComponent();
protected AppComponent getAppComponent() {
return ((App) getApplication()).getComponent();
}
}
public abstract class BaseFragment extends Fragment {
@Inject
Bus bus;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initDiComponent();
}
abstract protected void initDiComponent();
public T getActComponent(Class clazz){
Activity activity = getActivity();
HasComponent has = (HasComponent) activity;
return has.getComponent();
}
}
Теперь инициализация компонента во фрагменте выглядит так:
@Override
protected void initDiComponent() {
FirstFrComponent.HasFirstFrDepends depends = getActComponent(FirstFrComponent.HasFirstFrDepends.class);
DaggerFirstFrComponent.builder()
.hasFirstFrDepends(depends)
.build()
.inject(this);
}
Допустим, у нас есть менеджер, который инициализируется длительное время. Не хотелось бы, чтобы при запуске приложения все такие зависимости разом занимали главный поток. Нужно отложить внедрение этих зависимостей до момента их использования. Для этого в Dagger 2 есть интерфейсы Lazy и Provider, реализующие отложенную инициализацию зависимостей.
@Inject
Lazy managerA;
@Inject
Provider managerA;
Если ManagerA имеет некий скоуп, то их поведение идентично, но если скоуп отсутствует, Lazy после инициализации кеширует зависимость, а Provider генерирует каждый раз новую.
class ManagerA{
@Inject
ManagerA(){
Log.i("GTAG", "managerA init");
}
}
Log.i("GTAG", "managerA hashcode: " + managerA.get().hashCode());
Log.i("GTAG", "managerA hashcode: " + managerA.get().hashCode());
Log.i("GTAG", "managerA hashcode: " + managerA.get().hashCode());
Lazy-вариант:
managerA init
mAct managerA hashcode: 59563176
mAct managerA hashcode: 59563176
mAct managerA hashcode: 59563176
Provider-вариант:
managerA init
managerA hashcode: 162499239
managerA init
managerA hashcode: 2562900
managerA init
managerA hashcode: 32664317
Также сейчас ведутся разработки асинхронной инициализации зависимостей. Для того чтобы посмотреть на них, нужно добавить:
compile 'com.google.dagger: dagger-producers:2.0-beta'
И небольшой пример:
@ProducerModule
public class AsyncModule {
@Produces
ListenableFuture produceHugeManager() {
return Futures.immediateFuture(new HugeManager());
}
}
@ProductionComponent(modules = AsyncModule.class)
public interface AsyncComponent {
ListenableFuture hugeManager();
}
void initDiComponent() {
AsyncComponent component = DaggerAsyncComponent
.builder()
.asyncModule(new AsyncModule())
.executor(Executors.newSingleThreadExecutor())
.build();
ListenableFuture hugeManagerListenableFuture = component.hugeManager();
}
Получаем ListenableFuture, c которым уже можем работать, например, обернуть в Rx Observable. Готово!
Ниже приведены ссылки на проект с примерами и полезными презентациями:
Пример на GitHub
Официальная документация
Презентация от Jake Wharton
Хорошая презентация на русском
В следующих статьях мы готовы рассказать о наших мобильных разработках и об используемых технологиях. Спасибо за внимание и с наступающим Новым годом!