Всем привет!
Наконец-то подоспела третья часть цикла статей о Dagger 2!
Перед дальнейшим прочтением настоятельно рекомендую ознакомиться с первой и второй частями.
Большое спасибо за отзывы и комментарии. Я очень рад, что мои статьи действительно помогают разработчикам окунуться в мир Даггера. Именно это и придает силы творить для вас дальше.
В третьей части мы с вами рассмотрим различные интересные и немаловажные фичи библиотеки, которые могут вам очень пригодиться.
Вообще библиотека существует уже приличное время, но документация по-прежнему крайне отвратная. Разработчику, который только начинает свое знакомство с Даггером, я бы даже посоветовал не заглядывать в официальную документацию вначале, дабы не разочаровываться в этом жестком и несправедливом мире.
Есть, конечно, моменты, которые расписаны более-менее. Но вот всякие новые фичи описаны так, что мне приходилось методом проб и ошибок, залезая в сгенерированный код, самому разбираться, как оно все работает. Благо хорошие люди пишут хорошие статьи, но даже иногда они не дают четкого и ясного ответа сразу.
Итак, хватит разглагольствовать, и вперед к новым знаниям!
Qualifier annotation
В прошлой статье в комментариях попросили осветить данный вопрос. Не будем откладывать в долгий ящик.
Часто бывает, что нам необходимо провайдить несколько объектов одного типа. Например, мы хотим иметь в системе два Executor
: один однопоточный, другой с CachedThreadPool
. В этом случае нам приходит на помощь "qualifier annotation". Это кастомная аннотация, которая имеет в себе аннотацию @Qualifier
. Звучит немного как масло масляное, но на примере все гораздо проще.
В общем, Dagger2 предоставляет нам уже одну готовую "qualifier annotation", которой, пожалуй, вполне достаточно в повседневной жизни:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
/** The name. */
String value() default "";
}
А теперь посмотрим, как это все выглядит в бою:
@Module
public class AppModule {
@Provides
@Singleton
@Named("SingleThread")
public Executor provideSingleThreadExecutor() {
return Executors.newSingleThreadExecutor();
}
@Provides
@Singleton
@Named("MultiThread")
public Executor provideMultiThreadExecutor() {
return Executors.newCachedThreadPool();
}
}
public class MainActivity extends AppCompatActivity {
@Inject
@Named("SingleThread")
Executor singleExecutor;
@Inject
@Named("MultiThread")
Executor multiExecutor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyApplication.getInstance().getAppComponent().inject(this);
setContentView(R.layout.activity_main);
}
}
В итоге у нас два разных экземпляра (singleExecutor
, multiExecutor
) одного класса (Executor
). То, что нам и нужно!
Замечу, что объекты одного класса с аннотацией @Named
могут провайдиться также как с абсолютно разных и независимых компонентов, так и c зависимых друг от друга.
Отложенная инициализация
Одна из распространенных наших разработческих проблем — это долгий старт приложения. Обычно причина в одном — мы слишком много всего грузим и инициализируем при старте. Кроме того, Dagger2 строит граф зависимостей в основном потоке. И часто далеко не все конструируемые Даггером объекты нужны сразу же. Поэтому библиотека дает нам возможность отложить инициализацию объекта до первого вызова с помощью интерфейсов Provider<>
и Lazy<>
.
Сразу же обратим наш взор на пример:
@Module
public class AppModule {
@Provides
@Named("SingleThread")
public Executor provideSingleThreadExecutor() {
return Executors.newSingleThreadExecutor();
}
@Provides
@Named("MultiThread")
public Executor provideMultiThreadExecutor() {
return Executors.newCachedThreadPool();
}
}
public class MainActivity extends AppCompatActivity {
@Inject
@Named("SingleThread")
Provider<Executor> singleExecutorProvider;
@Inject
@Named("MultiThread")
Lazy<Executor> multiExecutorLazy;
@Inject
@Named("MultiThread")
Lazy<Executor> multiExecutorLazyCopy;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyApplication.getInstance().getAppComponent().inject(this);
setContentView(R.layout.activity_main);
//
Executor singleExecutor = singleExecutorProvider.get();
Executor singleExecutor2 = singleExecutorProvider.get();
//
Executor multiExecutor = multiExecutorLazy.get();
Executor multiExecutor2 = multiExecutorLazy.get();
Executor multiExecutor3 = multiExecutorLazyCopy.get();
}
}
Начнем с Provider<Executor> singleExecutorProvider
. До первого вызова singleExecutorProvider.get()
Даггер не инициализирует соответствующий Executor
. Но при каждом последующем вызове singleExecutorProvider.get()
будет создаваться новый экземпляр. Таким образом singleExecutor
и singleExecutor2
— это два разных объекта. Такое поведение по сути идентично поведению unscoped объекта.
В каких вообще ситуациях уместен Provider
? Он пригождается, когда мы провайдим какую-то мутабельную зависимость, меняющую свое состояние в течении времени, и при каждом обращении нам необходимо получать актуальное состояние. "Что за кривая архитектура?" — скажите вы, и я с вами соглашусь. Но при работе с legacy кодом и не такое увидишь.
Отмечу, что авторы библиотеки тоже не советуют злоупотреблять интерфейсом Provider
в тех местах, где достаточно обойтись обычным unscope, так как это чревато "кривой архитектурой", как говорилось выше, и трудно отлавливаемыми багами.
Теперь Lazy<Executor> multiExecutorLazy
и Lazy<Executor> multiExecutorLazyCopy
. Dagger2 инициализирует соответствующие Executor
только при первом вызове multiExecutorLazy.get()
и multiExecutorLazyCopy.get()
. Далее Даггер кэширует проинициализированные значения для каждого Lazy<>
и при втором вызове multiExecutorLazy.get()
и multiExecutorLazyCopy.get()
выдает закэшированные объекты.
Таким образом multiExecutor
и multiExecutor2
ссылаются на один объект, а multiExecutor3
на второй объект.
Но, если мы в AppModule
к методу provideMultiThreadExecutor()
добавим аннотацию @Singleton
, то объект будет кешироваться для всего дерева зависимостей, и multiExecutor
, multiExecutor2
, multiExecutor3
будут ссылаться на один объект.
Будьте внимательны.
Асинхронная загрузка
Мы подошли с вами к весьма нетривиальной задаче. А что, если мы хотим, чтобы конструирование графа зависимостей проходило в бэкграунде? Звучит многообещающе?
Да-да, я про Producers.
Честно скажу, это тема заслуживает вообще отдельного рассмотрения. Там много особенностей и нюансов. По ней достаточно хорошего материала. Сейчас же я коснусь только плюсов и минусов Producers.
Плюсы. Ну самый главный плюс — это загрузка в бэкграунде и возможность управлять этим процессом загрузки.
Минусы. Producers "тащат" за собой Guava, а это плюс 15 тысяч методов к апк. Но самое плохое, что применение Producers немного "портят" общую архитектуру и делают код более запутанным. Если у вас уже был Даггер, а потом вы решили перенести инициализацию объектов в бэкграунд, вам придется хорошенько постараться.
В официальной документации данная темы выделена в специальный раздел.
Но я очень рекомендую статьи Miroslaw Stanek. У него вообще очень хороший блог, и там много статей про Dagger2. Собственно, некоторые даже макеты картинок с прошлых статей я заимствовал у него.
Про Producers он пишет в этой статье.
А вот в следующей предлагает очень интересную альтернативу для загрузки дерева зависимостей в бэкграунде. На помощь приходит родная RxJava. Мне очень нравится его решение, так как оно полностью лишено недостатков использования Producers, но при этом решает вопрос асинхронной загрузки.
Один только минус: Мирослав не совсем верно применяет Observable.create(...)
. Но я об этом написал в комментарии к статье, так что обратите внимание обязательно.
А теперь посмотрим, как будет выглядеть тогда код для scope объекта (с "правильной" RxJava):
@Module
public class AppModule {
@Provides
@Singleton // or custom scope for "local" singletons
HeavyExternalLibrary provideHeavyExternalLibrary() {
HeavyExternalLibrary heavyExternalLibrary = new HeavyExternalLibrary();
heavyExternalLibrary.init(); //This method takes about 500ms
return heavyExternalLibrary;
}
@Provides
@Singleton // or custom scope for "local" singletons
Observable<HeavyExternalLibrary> provideHeavyExternalLibraryObservable(
final Lazy<HeavyExternalLibrary> heavyExternalLibraryLazy) {
return Observable.fromCallable(heavyExternalLibraryLazy::get)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
}
public class MainActivity extends AppCompatActivity {
@Inject
Observable<HeavyExternalLibrary> heavyExternalLibraryObservable;
//This will be injected asynchronously
HeavyExternalLibrary heavyExternalLibrary;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyApplication.getInstance().getAppComponent().inject(this);
setContentView(R.layout.activity_main);
// init HeavyExternalLibrary in background thread!
heavyExternalLibraryObservable.subscribe(
heavyExternalLibrary1 -> heavyExternalLibrary = heavyExternalLibrary1,
throwable -> {}
);
}
}
Обратите внимание на @Singleton
и интерфейс Lazy
в AppModule
. Lazy
как раз и гарантирует, что тяжеловесный объект будет проинициализирован, когда мы запросим, а затем закеширован.
А как нам быть, если мы хотим каждый раз получать новый экземпляр этого "тяжелого" объекта? Тогда стоит немного поменять AppModule
:
@Module
public class AppModule {
@Provides
// No scope!
HeavyExternalLibrary provideHeavyExternalLibrary() {
HeavyExternalLibrary heavyExternalLibrary = new HeavyExternalLibrary();
heavyExternalLibrary.init(); //This method takes about 500ms
return heavyExternalLibrary;
}
@Provides
@Singleton // or custom scope for "local" singletons
Observable<HeavyExternalLibrary> provideHeavyExternalLibraryObservable(
final Provider<HeavyExternalLibrary> heavyExternalLibraryLazy) {
return Observable.fromCallable(heavyExternalLibraryLazy::get)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
}
public class MainActivity extends AppCompatActivity {
@Inject
Observable<HeavyExternalLibrary> heavyExternalLibraryObservable;
//This will be injected asynchronously
HeavyExternalLibrary heavyExternalLibrary;
HeavyExternalLibrary heavyExternalLibraryCopy;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyApplication.getInstance().getAppComponent().inject(this);
setContentView(R.layout.activity_main);
// init HeavyExternalLibrary and heavyExternalLibraryCopy in background thread!
heavyExternalLibraryObservable.subscribe(
heavyExternalLibrary1 -> heavyExternalLibrary = heavyExternalLibrary1,
throwable -> {}
);
heavyExternalLibraryObservable.subscribe(
heavyExternalLibrary1 -> heavyExternalLibraryCopy = heavyExternalLibrary1,
throwable -> {}
);
}
}
Для метода provideHeavyExternalLibrary()
мы убрали scope, а в provideHeavyExternalLibraryObservable(final Provider<HeavyExternalLibrary> heavyExternalLibraryLazy)
используем Provider
вместо Lazy
. Таким образом heavyExternalLibrary
и heavyExternalLibraryCopy
в MainActivity
— это разные объекты.
А можно еще вообще весь процесс инициализации дерева зависимостей вынести в бэкграунд. Вы спросите, как? Очень даже легко. Сначала посмотрим на то, как было:
public class SplashActivity extends BaseActivity {
@Inject
SplashActivityPresenter presenter;
@Inject
AnalyticsManager analyticsManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupActivityComponent();
}
@Override
protected void setupActivityComponent() {
final SplashActivityComponent splashActivityComponent =
GithubClientApplication.get(SplashActivity.this)
.getAppComponent()
.plus(new SplashActivityModule(SplashActivity.this));
splashActivityComponent.inject(SplashActivity.this);
}
}
А теперь взглянем на обновленный метод void setupActivityComponent()
(с моими правками по RxJava):
@Override
protected void setupActivityComponent() {
Completable.fromAction(() -> {
final SplashActivityComponent splashActivityComponent =
GithubClientApplication.get(SplashActivity.this)
.getAppComponent()
.plus(new SplashActivityModule(SplashActivity.this));
splashActivityComponent.inject(SplashActivity.this);
})
.doOnCompleted(() -> {
//Here is the moment when injection is done.
analyticsManager.logScreenView(getClass().getName());
presenter.callAnyMethod();
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(() -> {}, throwable -> {});
}
Замеры
В прошлом разделе мы говорили про производительность при старте приложения. Однако мы знаем, что, если вопрос касается производительности и скорости, мы должны замерять! Полагаться на интуицию и чувство "вроде бы стало быстрее" нельзя.
И с этим нам снова поможет Мирослав в этой и этой статьях.
Чтобы мы все делали без него, вообще не представляю.
Новые интересные возможности
У Даггера появляются новые интересные фичи, обещающие нам облегчить жизнь. Но вот понять, как все работает и что же нам это все дает, — было задачей не из легких.
Ну что же, начнем!
@Reusable scope
Интересная аннотация. Позволяет экономить память, но при этом по сути не ограничена никаким scope
, что делает очень удобным переиспользование зависимостей в любых компонентах. То есть это нечто среднее между scope
и unscope
.
В доках пишут очень важный момент, который как-то не бросается в глаза с первого раза: "Для каждого компонента, который использует @Reusable
зависимость, данная зависимость кешируется отдельно". И мое дополнение: "В отличии от scope
аннотации, где объект кешируется при создании и его экземпляр используется дочерними и зависимыми компонентами"
А теперь сразу пример, чтобы все понять:
Наш главный компонент.
@Component(modules = {AppModule.class, UtilsModule.class})
@Singleton
public interface AppComponent {
FirstComponent.Builder firstComponentBuilder();
SecondComponent.Builder secondComponentBuilder();
}
У AppComponent
есть два Subcomponent
. Обратили внимание на эту конструкцию — FirstComponent.Builder
? О ней мы чуть позже.
Теперь посмотрим на UtilsModule
.
@Module
public class UtilsModule {
@Provides
@NonNull
@Reusable
public NumberUtils provideNumberUtils() {
return new NumberUtils();
}
@Provides
@NonNull
public StringUtils provideStringUtils() {
return new StringUtils();
}
}
NumberUtils
с аннотацией @Reusable
, а StringUtils
оставим unscoped
.
Далее у нас два Subcomponents
.
@FirstScope
@Subcomponent(modules = FirstModule.class)
public interface FirstComponent {
@Subcomponent.Builder
interface Builder {
FirstComponent.Builder firstModule(FirstModule firstModule);
FirstComponent build();
}
void inject(MainActivity mainActivity);
}
@SecondScope
@Subcomponent(modules = {SecondModule.class})
public interface SecondComponent {
@Subcomponent.Builder
interface Builder {
SecondComponent.Builder secondModule(SecondModule secondModule);
SecondComponent build();
}
void inject(SecondActivity secondActivity);
void inject(ThirdActivity thirdActivity);
}
Как мы видим, FirstComponent
инжектирует только в MainActivity
, а SecondComponent
— в SecondActivity
и ThirdActivity
.
Посмотрим код.
public class MainActivity extends AppCompatActivity {
@Inject
NumberUtils numberUtils;
@Inject
StringUtils stringUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyApplication.getInstance().getFirstComponent()
.inject(this);
// other...
}
}
public class SecondActivity extends AppCompatActivity {
@Inject
NumberUtils numberUtils;
@Inject
StringUtils stringUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
MyApplication.getInstance().getSecondComponent()
.inject(this);
// other...
}
}
public class ThirdActivity extends AppCompatActivity {
@Inject
NumberUtils numberUtils;
@Inject
StringUtils stringUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
MyApplication.getInstance().getSecondComponent()
.inject(this);
// other...
}
}
Коротко про навигацию. Из MainActivity
мы попадаем в SecondActivity
, а затем в ThirdActivity
. А теперь вопрос. Когда мы будем уже на третьем экране, сколько объектов NumberUtils
и StringUtils
будет создано?
Так как StringUtils
— unscoped
, то будет создано три экземпляра, то есть при каждой инъекции создается новый объект. Это мы знаем.
А вот объектов NumberUtils
будет два — один для FirstComponent
, а другой для SecondComponent
. И здесь я снова приведу основную мысль про @Reusable
с документации: "Для каждого компонента, который использует @Reusable
зависимость, данная зависимость кешируется отдельно!", в отличии от scope
аннотации, где объект кешируется при создании и его экземпляр используется дочерними и зависимыми компонентами
Но сами гугловцы предупреждают, что если вам необходим уникальный объект, который может быть еще и mutable, то используйте только scoped
аннотации.
Еще приведу ссылку на вопрос про сравнение @Singleton
и @Reusable
со SO.
@Subcomponent.Builder
Фича, которая делает код красивее.
Раньше, чтобы создать @Subcomponent
нам приходилось писать нечто такое:
@Component(modules = {AppModule.class, UtilsModule.class})
@Singleton
public interface AppComponent {
FirstComponent plusFirstComponent(FirstModule firstModule, SpecialModule specialModule);
}
@FirstScope
@Subcomponent(modules = {FirstModule.class, SpecialModule.class})
public interface FirstComponent {
void inject(MainActivity mainActivity);
}
Создание FirstComponent
:
appComponent
.plusFirstComponent(new FirstModule(), new SpecialModule());
Мне не нравилось в этом подходе то, что родительский компонент был загружен ненужными знаниями о модулях, которые используют дочерние сабкомпоненты. Ну и плюс передача большого количества аргументов выглядит не очень красиво, ведь для этого есть паттерн Builder.
Теперь стало красивее:
@Component(modules = {AppModule.class, UtilsModule.class})
@Singleton
public interface AppComponent {
FirstComponent.Builder firstComponentBuilder();
}
@FirstScope
@Subcomponent(modules = {FirstModule.class, SpecialModule.class})
public interface FirstComponent {
@Subcomponent.Builder
interface Builder {
FirstComponent.Builder firstModule(FirstModule firstModule);
FirstComponent.Builder specialModule(SpecialModule specialModule);
FirstComponent build();
}
void inject(MainActivity mainActivity);
}
Создание FirstComponent
теперь выглядит следующим образом:
appComponent
.firstComponentBuilder()
.firstModule(new FirstModule())
.specialModule(new SpecialModule())
.build();
Другое дело =)
static
Теперь у нас есть возможность делать вот так:
@Provides static User currentUser(AuthManager authManager) {
return authManager.currentUser();
}
То есть методы, отвечающие за провайдинг зависимостей в модулях, мы можем делать статическими. Я сначала не совсем понимал, а зачем это вообще нужно то? А оказывается, запрос на такую фичу существовал довольно давно, и есть ситуации, когда это выгодно.
На SO задали хороший вопрос на эту тему, мол, а чем собственно отличаются @Singleton
от @Provide static
. Чтобы хорошо понять эту разницу, нужно читать ответ на вопрос, параллельно экспериментируя и смотря сгенерированный код.
Итак, у нас есть вводная. Мы имеем три варианта одного и того же метода в модуле:
@Provides User currentUser(AuthManager authManager) {
return authManager.currentUser();
}
@Provides @Singleton User currentUser(AuthManager authManager) {
return authManager.currentUser();
}
@Provides static User currentUser(AuthManager authManager) {
return authManager.currentUser();
}
При этом authManager.currentUser()
в разные моменты времени может отдавать разные экземпляры.
Логичный вопрос: а чем эти методы отличаются.
В первом случае у нас классический unscope
. При каждом запросе будет отдаваться новый экземпляр authManager.currentUser()
(точнее новая ссылка на currentUser
).
Во втором случае при первом запросе будет закеширована ссылка на currentUser
, и при каждом новом запросе будет отдаваться эта ссылка. То есть, если поменялся currentUser
в AuthManager
, то отдаваться то будет старая ссылка на невалидный уже экземпляр.
Третий случай уже интереснее. Данный метод по поведению аналогичен unscope
, то есть при каждом запросе будет отдаваться новая ссылка. Это первое отличие от @Singleton
, который кеширует объекты. Таким образом размещать в @Provide static
методе инициализацию объекта не совсем уместно.
Но в чем тогда @Provide static
отличается от unscope
?
Допустим у нас есть такой модуль:
@Module
public class AuthModule {
@Provides
User currentUser(AuthManager authManager) {
return authManager.currentUser();
}
}
AuthManager
поставляется из другого модуля в качестве Singleton
.
Теперь быстро окинем взглядом сгенерированный код AuthModule_CurrentUserFactory
(в студии просто поставьте курсор на currentUser
и нажмите Ctrl+B):
@Generated(
value = "dagger.internal.codegen.ComponentProcessor",
comments = "http://ift.tt/1qApJk6"
)
public final class AuthModule_CurrentUserFactory implements Factory<User> {
private final AuthModule module;
private final Provider<AuthManager> authManagerProvider;
public AuthModule_CurrentUserFactory(
AuthModule module, Provider<AuthManager> authManagerProvider) {
assert module != null;
this.module = module;
assert authManagerProvider != null;
this.authManagerProvider = authManagerProvider;
}
@Override
public User get() {
return Preconditions.checkNotNull(
module.currentUser(authManagerProvider.get()),
"Cannot return null from a non-@Nullable @Provides method");
}
public static Factory<User> create(AuthModule module, Provider<AuthManager> authManagerProvider) {
return new AuthModule_CurrentUserFactory(module, authManagerProvider);
}
/** Proxies {@link AuthModule#currentUser(AuthManager)}. */
public static User proxyCurrentUser(AuthModule instance, AuthManager authManager) {
return instance.currentUser(authManager);
}
}
А если добавить static к currentUser
:
@Module
public class AuthModule {
@Provides
static User currentUser(AuthManager authManager) {
return authManager.currentUser();
}
}
То получим:
@Generated(
value = "dagger.internal.codegen.ComponentProcessor",
comments = "http://ift.tt/1qApJk6"
)
public final class AuthModule_CurrentUserFactory implements Factory<User> {
private final Provider<AuthManager> authManagerProvider;
public AuthModule_CurrentUserFactory(Provider<AuthManager> authManagerProvider) {
assert authManagerProvider != null;
this.authManagerProvider = authManagerProvider;
}
@Override
public User get() {
return Preconditions.checkNotNull(
AuthModule.currentUser(authManagerProvider.get()),
"Cannot return null from a non-@Nullable @Provides method");
}
public static Factory<User> create(Provider<AuthManager> authManagerProvider) {
return new AuthModule_CurrentUserFactory(authManagerProvider);
}
/** Proxies {@link AuthModule#currentUser(AuthManager)}. */
public static User proxyCurrentUser(AuthManager authManager) {
return AuthModule.currentUser(authManager);
}
}
Обратите внимание, что в варианте со static
нет AuthModel
. Таким образом, статический метод дергается компонентом напрямую, минуя модуль. А если в модуле только одни статические методы, то экземпляр модуля даже не создается.
Экономия и минус лишние вызовы. Собственно у нас выигрыш по производительности.
Также пишут, что вызов статического метода на 15-20% быстрее вызова аналогичного нестатического метода. Если я ошибаюсь, iamironz поправит меня. Уж он то точно знает, а если нужно, и замерит.
@Binds + Inject конструктора
Мегаудобная связка, которая значительно уменьшает boilerplate-code. На заре изучения Даггера я не понимал, зачем нужны инъекции конструктора. Что и откуда берется. А тут еще появился @Binds. Но все на самом деле довольно просто. Спасибо за помощь Владимиру Тагакову и вот этой статье.
Рассмотрим типичную ситуацию. Есть интерфейс Презентера и его реализация:
public interface IFirstPresenter {
void foo();
}
public class FirstPresenter implements IFirstPresenter {
public FirstPresenter() {}
@Override
public void foo() {}
}
Мы, как белые люди, провайдим все это дело в модуле и инжектим интерфейс Презентера в активити:
@Module
public class FirstModule {
@Provides
@FirstScope
public IFirstPresenter provideFirstPresenter() {
return new FirstPresenter();
}
}
public class MainActivity extends AppCompatActivity {
@Inject
IFirstPresenter firstPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyApplication.getInstance().getFirstComponent()
.inject(this);
// others
}
}
Допустим, что наш FirstPresenter
нуждается в классах-помощниках, которым он делегирует часть работы. Для этого необходимо в модуле создать еще два метода, которые будут провайдить новые классы, потом изменить конструктор FirstPresenter
, а следственно и обновить соответствующий метод в модуле.
Модуль будет такой:
@Module
public class FirstModule {
@Provides
@FirstScope
public HelperClass1 provideHelperClass1() {
return new HelperClass1();
}
@Provides
@FirstScope
public HelperClass2 provideHelperClass2() {
return new HelperClass2();
}
@Provides
@FirstScope
public IFirstPresenter provideFirstPresenter(
HelperClass1 helperClass1, HelperClass2 helperClass2) {
return new FirstPresenter(helperClass1, helperClass2);
}
}
И так вот каждый раз, если нужно добавить какой-то класс и "расшарить" его другим. Модуль "загрязняется" очень быстро. И как-то слишком много кода, не находите? Но есть решение, которое существенно уменьшает код.
Во-первых, если нам необходимо создать зависимость и отдавать готовый класс, а не интерфейс (HelperClass1
и HelperClass2
), мы можем прибегнуть к инъекции конструктора.
Выглядеть это будет следующим образом:
@FirstScope
public class HelperClass1 {
@Inject
public HelperClass1() {
}
}
@FirstScope
public class HelperClass2{
@Inject
public HelperClass2() {
}
}
Обратите внимание, что к классам была добавлена аннотация @FirstScope
, таким образом Даггер понимает, в какое дерево зависимостей отнести данные классы.
Теперь с модуля мы можем смело убирать провайдинг HelperClass1
и HelperClass2
:
@Module
public class FirstModule {
@Provides
@FirstScope
public IFirstPresenter provideFirstPresenter(
HelperClass1 helperClass1, HelperClass2 helperClass2) {
return new FirstPresenter(helperClass1, helperClass2);
}
}
Как можно еще уменьшить код в модуле? Вот здесь применим @Binds
:
@Module
public abstract class HelperModule {
@FirstScope
@Binds
public abstract IFirstPresenter provideFirstPresenter(FirstPresenter firstPresenter);
}
А в FirstPresenter
сделаем инъекцию конструктора:
@FirstScope
public class FirstPresenter implements IFirstPresenter {
private HelperClass1 helperClass1;
private HelperClass2 helperClass2;
@Inject
public FirstPresenter(HelperClass1 helperClass1, HelperClass2 helperClass2) {
this.helperClass1 = helperClass1;
this.helperClass2 = helperClass2;
}
@Override
public void foo() {}
}
Какие здесь новшества?
HelperModule
стал у нас абстрактным, как и метод provideFirstPresenter
. У provideFirstPresenter
убрали аннотацию @Provide
, зато добавили @Binds
. А в аргументы передаем не необходимые зависимости, а конкретную реализацию!
У FirstPresenter
добавилась scope
аннотация — @FirstScope
, по которой Даггер понимает, куда отнести данный класс. Также к конструктору добавили аннотацию @Inject
.
Стало намного чище, и добавлять новые зависимости стало еще проще!
О чем еще не сказано
Далее я приведу еще список фич с коротким описанием и ссылками на качественное объяснение:
-
Muitibindings. Позволяет "байндить" объекты в коллекции (
Set
иMap
). Подходит для реализации архитектуры расширения ("plugin architecture"). Крайне рекомендую вот это очень подробное описание с азов. Более интересные примеры применения Muitibindings можно найти в статьях Мирослава тут и тут. И еще в придачу ссылка на официальную документацию. Так что мне даже нечего добавить по данному вопросу. -
Releasable references. Если уж с памятью совсем беда. С помощью соответствующих аннотаций мы помечаем объекты, которыми можем пожертвовать при недостатке памяти. Вот такой вот хак.
В доках (подраздел Releasable references) вполне все понятно описано, как ни странно. -
Тестирование. Конечно же, для Unit-тестирования Даггер не нужен. А вот для функциональных, интеграционных и UI тестов может пригодиться возможность подмены определенных модулей. Очень здорово эту тему раскрывает Artem_zin в своей статье и примере. В документации выделен раздел по вопросу тестирования. Но опять-таки гугловцы не могут нормально описать, как именно подменить компонент. Как правильно создать фэйковые модули и подставить их. Для подмены компонента (отдельных модулей) я пользуюсь способом Артема. Да, хотелось бы, чтобы можно было создать отдельным классом тестовый компонент и отдельными классами тестовые модули, и красиво все это подключить в тестовом
Application
файле. Может кто знает? -
@BindsOptionalOf. Работает вместе с
Optional
от Java 8 или Guava, что делает данную фичу уже труднодоступной для нас. Если интересно, в конце документации можно найти описание. - @BindsInstance. К сожалению, в dagger 2.8 мне данная фича оказалась недоступной. Основной посыл ее в том, что хватит передавать какие-либо объекты через конструктор модуля. Очень распространенный пример, когда через конструктор
AppComponent
передается глобальныйContext
. Так вот с этой аннотацией такого делать станет не нужно. В конце документации есть пример.
Ну вот и все! Вроде все моменты удалось осветить. Если что-то пропустил или недостаточно описал, пишите! Исправим. Также рекомендую группу по Dagger2 в Телеграме, где ваши вопросы не останутся без ответов.
Кроме того, правильное применение библиотеки очень связано с чистой архитектурой. Поэтому вот вам и группа по архитектуре. И да, скоро на AndroidDevPodcast планируется выпуск, посвященный Даггеру. Следите за новостями!
Комментарии (0)