Dagger 2. Частина третя. Нові грані можливого

Всім привіт!
Нарешті прийшла третя частина циклу статей про Dagger 2!
Перед подальшим прочитанням настійно рекомендую ознайомитися з першою і другий частинами.
Велике спасибі за відгуки та коментарі. Я дуже радий, що мої статті дійсно допомагають розробникам зануритися в світ Даггера. Саме це і додає сили творити для вас далі.
У третій частині ми з вами розглянемо різні цікаві і важливі фічі бібліотеки, які можуть вам дуже стати в нагоді.
Взагалі бібліотека існує вже пристойний час, але документація раніше вкрай отвратная. Розробнику, який тільки починає своє знайомство з Даггером, я б навіть порадив не заглядати в офіційну документацію спочатку, щоб не розчаровуватися в цьому жорсткому і несправедливому світі.
Є, звичайно, моменти, які розписані більш-менш. Але от всякі нові фічі описані так, що мені доводилося методом проб і помилок, накопичуючи згенерований код, самому розбиратися, як воно все працює. Благо хороші люди пишуть гарні статті, але навіть іноді вони не дають чіткого і ясного відповіді відразу.
Отже, вистачить просторікувати, і вперед до нових знань!
Qualifier annotation
У минулій статті в коментарях попросили висвітлити дане питання. Не будемо відкладати в довгий ящик.
Часто буває, що нам необхідно провайдить кілька об'єктів одного типу. Наприклад, ми хочемо мати в системі два
Executor
: один однопотоковий, інший з
CachedThreadPool
. У цьому випадку нам приходить на допомогу "qualifier annotation". Це кастомний анотація, яка має в собі анотацію
@Qualifier
. Звучить трохи як масло масляне, але на прикладі все набагато простіше.
загалом, Dagger2 надає нам уже одну готову "qualifier annotation", якої, мабуть, цілком достатньо в повсякденному житті:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {

/** The name. */
String value() default "";
}

А тепер подивимося, як це все виглядає в бою:
Qualifier annotation приклад
@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, але при цьому вирішує питання асинхронної завантаження.
Один тільки мінус: Мирослав не зовсім вірно застосовує
Вами.create(...)
. Але я про це написав у коментарі до статті, так що зверніть увагу обов'язково.
А тепер подивимося, як буде виглядати тоді код scope об'єкта (з "правильної" RxJava):
Приклад зі scope
@Module
public class AppModule {

@Provides
@Singleton // custom or scope for "local" singletons
HeavyExternalLibrary provideHeavyExternalLibrary() {
HeavyExternalLibrary heavyExternalLibrary = new HeavyExternalLibrary();
heavyExternalLibrary.init(); //This method takes about 500ms
return heavyExternalLibrary;
}

@Provides 
@Singleton // custom or scope for "local" singletons
Вами<HeavyExternalLibrary> provideHeavyExternalLibraryObservable(
final Lazy<HeavyExternalLibrary> heavyExternalLibraryLazy) {
return Вами.fromCallable(heavyExternalLibraryLazy::get)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}

}

public class MainActivity extends AppCompatActivity {

@Inject
Вами<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
:
Приклад з unscope
@Module
public class AppModule {

@Provides
// No scope!
HeavyExternalLibrary provideHeavyExternalLibrary() {
HeavyExternalLibrary heavyExternalLibrary = new HeavyExternalLibrary();
heavyExternalLibrary.init(); //This method takes about 500ms
return heavyExternalLibrary;
}

@Provides
@Singleton // custom or scope for "local" singletons
Вами<HeavyExternalLibrary> provideHeavyExternalLibraryObservable(
final Provider<HeavyExternalLibrary> heavyExternalLibraryLazy) {
return Вами.fromCallable(heavyExternalLibraryLazy::get)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}

}

public class MainActivity extends AppCompatActivity {

@Inject
Вами<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
— це різні об'єкти.
А можна ще взагалі весь процес ініціалізації дерева залежностей винести в бекграунд. Ви запитаєте, як? Дуже навіть легко. Спочатку подивимося на те, як було:
SplashActivity зі статті Мирослава
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):
void setupActivityComponent()
@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):
Unscope
@Generated(
value = "dagger.internal.codegen.ComponentProcessor",
comments = "https://google.github.io/dagger"
)
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();
}
}

То отримаємо:
static
@Generated(
value = "dagger.internal.codegen.ComponentProcessor",
comments = "https://google.github.io/dagger"
)
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
.
Стало набагато чистіше, і додавати нові залежності стало ще простіше!
Про що ще не сказано
Далі я наведу ще список фіч з коротким описом і посиланнями на якісне пояснення:
  1. Muitibindings. Дозволяє "байндить" об'єкти в колекції (
    Set
    та
    Map
    ). Підходить для реалізації архітектури розширення ("plugin architecture"). Вкрай рекомендую ось це дуже докладний опис з азів. Більш цікаві приклади застосування Muitibindings можна знайти в статтях Мирослава тут і тут. І ще в додачу посилання на офіційну документацію. Так що мені навіть нічого додати з даного питання.
  2. Releasable references. Якщо вже з пам'яттю зовсім біда. З допомогою відповідних анотацій ми помічаємо об'єкти, якими можемо пожертвувати при нестачі пам'яті. Ось такий хак.
    доках (підрозділ Releasable references) все цілком зрозуміло описано, як не дивно.
  3. Тестування. Звичайно ж, для Unit-тестування Даггер не потрібен. А ось для функціональних, інтеграційних та UI тестів може знадобитися можливість підміни певних модулів. Дуже здорово цю тему розкриває Artem_zin у своїй статті і приклад. В документації виділений розділ з питання тестування. Але знову-таки гугловцы не можуть нормально описати, як саме підмінити компонент. Як правильно створити фейкові модулі і підставити їх. Для підміни компонента (окремих модулів) я користуюся способом Артема. Так, хотілося б, щоб можна було створити окремим класом тестовий компонент і окремими класами тестові модулі, і красиво все це підключити в тестовому
    Application
    файлі. Може хто знає?
  4. @BindsOptionalOf. Працює разом з
    Optional
    від Java 8 або Guava, що робить цю фічу вже важкодоступній для нас. Якщо цікаво, в кінці документації можна знайти опис.
  5. @BindsInstance. На жаль, в dagger 2.8 мені ця фіча виявилася недоступною. Основний посил її в тому, що вистачить передавати об'єкти через конструктор модуля. Дуже поширений приклад, коли через конструктор
    AppComponent
    передається глобальний
    Context
    . Так от з цієї анотацією такого робити не стане потрібно. В кінці документації є приклад.
Ну от і все! Начебто всі моменти вдалося висвітлити. Якщо щось пропустив або недостатньо описав, пишіть! Виправимо. Також рекомендую групу з Dagger2 в Телеграме, де ваші питання не залишаться без відповідей.
Крім того, правильне застосування бібліотеки дуже пов'язано з чистою архітектурою. Тому ось вам і група по архітектурі. І так, скоро на AndroidDevPodcast планується випуск, присвячений Даггеру. Стежте за новинами!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.