Builder
У цьому уроці ми детально розглянемо білдери: як вони генеруються даггером, як можна використовувати свій білдер, як за допомогою анотації @BindsInstance передавати об'єкти в компонент, минаючи модулі. Крім цього, розглянемо варіант архітектурного рішення Dagger 2 + MVP, яке дасть вам змогу зберігати презентер під час повороту екрана. Навчимося створювати сабкомпоненти за допомогою білдерів і аргументу subcomponents в анотації @Module.
Коли дагер генерує клас компонента, він створює в ньому білдер. Цей білдер ми використовуємо під час створення компонента, якщо нам необхідно вручну передати якийсь модуль. Якщо ж нам не треба передавати модулі, то компонент ми зазвичай створюємо методом create. Давайте докладніше подивимося в чому різниця між двома цими способами створення.
Наприклад, нам потрібен якийсь компонент AppComponent, від якого ми хочемо отримувати об'єкт SomeObject. Створюємо AppModule з методом створення SomeObject:
@Module()
public class AppModule {
@Provides
SomeObject provideSomeObject() {
return new SomeObject();
}
}
Створюємо інтерфейс AppComponent і прописуємо в ньому модуль і метод для отримання SomeObject
@Component(modules = {AppModule.class})
public interface AppComponent {
SomeObject getSomeObject();
}
Тепер щоб створити компонент, нам треба буде написати такий код:
AppComponent appComponent = DaggerAppComponent.create();
Ми викликаємо метод create і не використовуємо жодного білдера. Але давайте заглянемо, що відбувається в методі DaggerAppComponent.create:
public static AppComponent create() {
return builder().build();
}
Для створення об'єкта все одно використовується builder. Видно, що в цьому випадку білдер не використовує параметри, а відразу йде виклик методу build.
Дивимося код білдера в DaggerAppComponent.java:
public static final class Builder {
private AppModule appModule;
private Builder() {}
public AppComponent build() {
if (appModule == null) {
this.appModule = new AppModule();
}
return new DaggerAppComponent(this);
}
public Builder appModule(AppModule appModule) {
this.appModule = Preconditions.checkNotNull(appModule);
return this;
}
}
Зверніть увагу, у вас є можливість використовувати метод appModule, щоб дати компоненту свій екземпляр модуля AppModule. Можливість є, але немає суворої необхідності використовувати її. Метод build перевірить, і якщо визначить, що ви не надали об'єкт AppModule, то він просто створить його сам.
Тобто коли ви викликаєте DaggerAppComponent.create, то йде виклик builder().build(), який сам створює об'єкт AppModule.
Тепер давайте ускладнимо приклад. Перепишемо AppModule так, щоб йому при створенні був потрібен об'єкт SomeObject.
@Module()
public class AppModule {
private final SomeObject someObject;
public AppModule(SomeObject someObject) {
this.someObject = someObject;
}
@Provides
SomeObject provideSomeObject() {
return someObject;
}
}
Якщо тепер перекомпілювати проєкт, то код:
AppComponent appComponent = DaggerAppComponent.create();
перестане працювати, тому що більше не існує методу DaggerAppComponent.create. Тому що тепер не можна просто так викликати builder().build() і створити AppModule за допомогою дефолтного конструктора.
Давайте подивимося, як змінився код білдера в DaggerAppComponent.java:
public static final class Builder {
private AppModule appModule;
private Builder() {}
public AppComponent build() {
if (appModule == null) {
throw new IllegalStateException(AppModule.class.getCanonicalName() + " must be set");
}
return new DaggerAppComponent(this);
}
public Builder appModule(AppModule appModule) {
this.appModule = Preconditions.checkNotNull(appModule);
return this;
}
}
Білдер знає, що він не зможе сам створити AppModule. І нам тепер строго необхідно викликати метод appModule, щоб передати AppModule білдеру, інакше ми отримаємо IllegalStateException під час виклику методу build.
Тобто тепер створити компонент ми можемо тільки так:
SomeObject someObject = new SomeObject();
AppModule appModule = new AppModule(someObject);
AppComponent appComponent = DaggerAppComponent.builder().appModule(appModule).build();
У цьому випадку ми явно використовуємо білдер.
@Component.Builder
Даггер дає нам можливість самим описати інтерфейс білдера компонента. Для цього використовується анотація @Component.Builder:
@Component(modules = {AppModule.class})
public interface AppComponent {
SomeObject getSomeObject();
@Component.Builder
interface MyBuilder {
AppComponent letsBuildThisComponent();
MyBuilder methodForSettingAppModule(AppModule appModule);
}
}
В інтерфейсі компонента ми описуємо інтерфейс для білдера. У ньому два методи.
Як мінімум один метод в інтерфейсі білдера має бути завжди - це аналог методу build. Це має бути метод без аргументів, який повертає компонент. Назвати його можна як завгодно. У цьому прикладі він названий letsBuildThisComponent.
Щоб передати модуль, необхідно описати метод, який на вхід прийме цей модуль, а поверне білдер. У цьому прикладі це метод methodForSettingAppModule.
Ім'я інтерфейсу білдера ви можете вибрати, яке вам зручно.
Скомпілюємо проєкт і подивимося на код білдера всередині DaggerAppComponent.java
private static final class Builder implements AppComponent.MyBuilder {
private AppModule appModule;
@Override
public AppComponent letsBuildThisComponent() {
if (appModule == null) {
throw new IllegalStateException(AppModule.class.getCanonicalName() + " must be set");
}
return new DaggerAppComponent(this);
}
@Override
public Builder methodForSettingAppModule(AppModule appModule) {
this.appModule = Preconditions.checkNotNull(appModule);
return this;
}
}
Builder реалізує інтерфейс, який ми описували в компоненті - AppComponent.MyBuilder. А створення компонента тепер виглядає так:
SomeObject someObject = new SomeObject();
AppModule appModule = new AppModule(someObject);
AppComponent appComponent = DaggerAppComponent.builder().methodForSettingAppModule(appModule).letsBuildThisComponent();
@BindsInstance
Повернемося до об'єкта SomeObject і до випадку, коли ми самі створюємо цей об'єкт, передаємо його в модуль, а модуль передаємо в білдер компонента. При використанні свого білдера, ми можемо уникнути використання модуля, і відразу передавати об'єкт SomeObject в компонент, використовуючи білдер.
Давайте реалізуємо це в нашому прикладі:
AppModule
@Module()
public class AppModule {
}
Прибираємо весь код, пов'язаний з SomeObject, з AppModule. У підсумку, в цьому прикладі модуль залишився зовсім порожнім. Це позначиться на білдері, трохи далі подивимося, як.
Перепишемо інтерфейс білдера в компоненті AppComponent:
@Component(modules = {AppModule.class})
public interface AppComponent {
SomeObject getSomeObject();
@Component.Builder
interface MyBuilder {
AppComponent letsBuildThisComponent();
@BindsInstance
MyBuilder setMyInstanceOfSomeObject(SomeObject someObject);
}
}
По-перше, ми прибрали метод для передання в білдер модуля AppModule, тому що тепер білдер сам зможе його створити за допомогою дефолтного конструктора, і нам уже немає необхідності створювати його вручну.
По-друге, додаємо метод setMyInstanceOfSomeObject, щоб передати компоненту об'єкт SomeObject безпосередньо, минаючи модулі. До цього методу необхідно додати анотацію @BindsInstance 3. Вона доступна починаючи з версії 2.9.
Скомпілюємо проєкт і подивимося, як тепер виглядає код білдера в DaggerAppComponent.java
private static final class Builder implements AppComponent.MyBuilder {
private SomeObject setMyInstanceOfSomeObject;
@Override
public AppComponent letsBuildThisComponent() {
if (setMyInstanceOfSomeObject == null) {
throw new IllegalStateException(SomeObject.class.getCanonicalName() + " must be set");
}
return new DaggerAppComponent(this);
}
@Override
public Builder setMyInstanceOfSomeObject(SomeObject someObject) {
this.setMyInstanceOfSomeObject = Preconditions.checkNotNull(someObject);
return this;
}
}
AppModule абсолютно зник із білдера. Так вийшло тому, що модуль зараз абсолютно порожній і компонент зрозумів, що такий модуль йому просто не потрібен. Якби в AppModule створювалися якісь об'єкти, то він, звісно, залишився б у білдері.
Зате бачимо, що з'явився метод setMyInstanceOfSomeObject, який чекає від нас об'єкт SomeObject. Цей метод є обов'язковим при створенні компонента, інакше буде IllegalStateException при виклику build.
Код створення компонента тепер має такий вигляд:
SomeObject someObject = new SomeObject();
AppComponent appComponent = DaggerAppComponent.builder().setMyInstanceOfSomeObject(someObject).letsBuildThisComponent();
Без використання модулів ми змогли передати об'єкт у компонент.
Сабкомпоненти
Для сабкомпонентів є аналогічна анотація: @Subcomponent.Builder, яка дозволяє описати свій білдер. Крім цього, свій білдер дає змогу трохи змінити схему створення сабкомпонента. Щоб показати це досить наочно, розглянемо приклад застосунку.
Цей застосунок містить у собі варіант архітектурного рішення, що дає змогу зберігати сабкомпонент під час повороту екрана. Це може бути корисним, якщо ви використовуєте MVP, і вам треба уникнути повторного створення презентера.
Вихідні коди ви можете знайти тут: https://github.com/startandroid/Dagger2_SubcomponentBuilderProject 5
Там створено дві гілки:
subcomponents_old- старий спосіб створення сабкомпонентів, за допомогою методуcreateу батьківському компонентіsubcomponents_builder 1- новий спосіб створення сабкомпонентів за допомогою свого білдера
Загальна схема застосунку однакова для обох гілок. Коротенько розповім про неї, щоб легше було розуміти код.
У застосунку є два екрани: FirstActivity і SecondActivity, для яких існують дагер-компоненти FirstActivityComponent і SecondActivitySubcomponent. Під час створення Activity отримує компонент і з його допомогою виконує inject, тобто заповнює себе необхідними об'єктами. У нашому прикладі в Activity потрібен тільки презентер.
Компоненти FirstActivityComponent і SecondActivitySubcomponent є сабкомпонентами для основного компонента програми: AppComponent.
Весь код по роботі з компонентами (і сабкомпонентами) винесено в окремий клас ComponentsHolder, щоб не смітити в Application класі. ComponentsHolder зберігає в собі об'єкти компонентів, а Activity завжди може створити/отримати в нього потрібний компонент або обнулити його (= null).
На прикладі FirstActivity розглянемо життєвий цикл зв'язки Activity + сабкомпонент.
FirstActivity під час створення просить ComponentsHolder дати їй FirstActivityComponent. ComponentsHolder перевіряє, якщо у нього вже існує такий об'єкт, то він просто повертає його. Якщо ні - то створює новий об'єкт і повертає його. FirstActivity за допомогою цього компонента виконує inject і екран готовий до роботи.
Коли FirstActivity розуміє, що воно зараз буде закрито, воно просить ComponentsHolder обнулити компонент FirstActivityComponent. Це відбувається тільки при повному закритті Activity. Якщо Activity закривається з подальшим перестворенням (під час повороту екрана, наприклад), то компонент не обнуляється.
Тобто якщо ви закрили екран і знову його відкриваєте, то ComponentsHolder створить новий екземпляр FirstActivityComponent, і FirstActivity працюватиме з ним. Далі, якщо ви повернете екран, то FirstActivity під час відтворення знову отримає цей самий екземпляр FirstActivityComponent. Відповідно, ви зможете дістати з компонента всі необхідні вам об'єкти, оновити UI і продовжити роботу, ніби нічого й не сталося. А вже під час відходу з екрана FirstActivity, компонент FirstActivityComponent буде обнулено в ComponentsHolder, і під час наступного запиту (під час нового відкриття екрана FirstActivity) його буде створено знову.
Давайте подивимося ключові фрагменти коду, які реалізують цю схему. Спочатку розглядаємо гілку subcomponents_old
Код компонентів
FirstActivityComponent.java:
@FirstActivityScope
@Subcomponent(modules = FirstActivityModule.class)
public interface FirstActivityComponent {
void inject(FirstActivity firstActivity);
}
SecondActivityComponent.java:
@SecondActivityScope
@Subcomponent(modules = SecondActivityModule.class)
public interface SecondActivityComponent {
void inject(SecondActivity secondActivity);
}
AppComponent.java:
@AppScope
@Component(modules = AppModule.class)
public interface AppComponent {
FirstActivityComponent createFirstActivityComponent();
SecondActivityComponent createSecondActivityComponent(SecondActivityModule secondActivityModule);
}
Два сабкомпоненти для Activity і основний компонент AppCompoment, у якому створюються ці сабкомпоненти. Під час створення SecondActivityComponent ми вказуємо, що треба буде вручну створювати модуль SecondActivityModule.
Подивимося код цього модуля:
@Module
public class SecondActivityModule {
private final Bundle args;
public SecondActivityModule(Bundle args) {
this.args = args;
}
@SecondActivityScope
@Provides
SecondActivityPresenter provideSecondActivityPresenter() {
return new SecondActivityPresenter(args);
}
}
Щоб створити презентер, модулю знадобиться Bundle. У цьому конкретному прикладі презентер не використовуватиме ці дані, але в робочих проєктах такий спосіб цілком може бути використаний для передачі якихось початкових даних у презентер, тому я вирішив реалізувати це тут.
Дивимося код, пов'язаний із ComponentsHolder
App.java:
public class App extends Application {
private ComponentsHolder componentsHolder;
public static App getApp(Context context) {
return (App)context.getApplicationContext();
}
@Override
public void onCreate() {
super.onCreate();
componentsHolder = new ComponentsHolder(this);
componentsHolder.init();
}
public ComponentsHolder getComponentsHolder() {
return componentsHolder;
}
}
У Application класі створюємо ComponentsHolder і виконуємо його метод init. Для доступу до ComponentsHolder використовується метод getComponentsHolder.
ComponentsHolder.java:
public class ComponentsHolder {
private final Context context;
private AppComponent appComponent;
private FirstActivityComponent firstActivityComponent;
private SecondActivityComponent secondActivityComponent;
public ComponentsHolder(Context context) {
this.context = context;
}
void init() {
appComponent = DaggerAppComponent.builder().appModule(new AppModule(context)).build();
}
// AppComponent
public AppComponent getAppComponent() {
return appComponent;
}
// FirstActivityComponent
public FirstActivityComponent getFirstActivityComponent() {
if (firstActivityComponent == null) {
firstActivityComponent = getAppComponent().createFirstActivityComponent();
}
return firstActivityComponent;
}
public void releaseFirstActivityComponent() {
firstActivityComponent = null;
}
//SecondActivityComponent
public SecondActivityComponent getSecondActivityComponent(Bundle args) {
if (secondActivityComponent == null) {
secondActivityComponent = getAppComponent().createSecondActivityComponent(new SecondActivityModule(args));
}
return secondActivityComponent;
}
public void releaseSecondActivityComponent() {
secondActivityComponent = null;
}
}
У методі init створюємо AppComponent, і він завжди буде доступний через метод getAppComponent.
Далі для кожного Activity створено два методи get і release. У методі get за допомогою appComponent створюється сабкомпонент, якщо він не був створений раніше. А в методі release посилання на компонент обнуляється.
Дивимося код SecondActivity:
public class SecondActivity extends AppCompatActivity {
public static final String EXTRA_ARGS = "args";
@Inject
SecondActivityPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_activity);
Bundle args = getIntent().getBundleExtra(EXTRA_ARGS);
App.getApp(this).getComponentsHolder().getSecondActivityComponent(args).inject(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (isFinishing()) {
App.getApp(this).getComponentsHolder().releaseSecondActivityComponent();
}
}
}
У методі onCreate ми отримуємо Bundle з Intent і використовуємо його під час створення модуля. А модуль потім використовує цей Bundle для створення презентера.
Просимо компонент у ComponentsHolder, викликаємо метод inject і отримуємо презентер у змінну
@Inject
SecondActivityPresenter presenter;
У методі onDestroy перевіряємо, що Activity закривається назовсім, і, в цьому разі, просимо ComponentsHolder обнулити компонент.
Схема загалом нескладна, але забезпечить вам збереження презентера при поворотах екрана.
У цьому прикладі getIntent().getBundleExtra(EXTRA_ARGS) нічого не поверне, тому що FirstActivity нічого туди не передає. Я тут витягую Bundle з Intent тільки для того, щоб показати, як можна передати параметри в презентер.
Тепер розглянемо гілку subcomponents_builder
У пакеті base лежать три інтерфейси, які довелося створити, щоб у підсумку отримати гарне універсальне рішення для створення сабкомпонентів.
ActivityModule.java:
public interface ActivityModule {
}
Модуль сабкомпонента має реалізувати цей порожній інтерфейс.
ActivityComponent.java:
public interface ActivityComponent<A> {
void inject(A activity);
}
Сабкомпонент має успадковувати цей інтерфейс. У ньому всього один метод, який буде інджектити вказане Activity.
ActivityComponentBuilder.java:
public interface ActivityComponentBuilder<C extends ActivityComponent, M extends ActivityModule> {
C build();
ActivityComponentBuilder<C,M> module(M module);
}
Білдер, який ми будемо описувати для сабкомпонента, має успадковувати цей інтерфейс. Він містить методи, які ми вже розглядали раніше на початку уроку: build - для створення компонента C, і module - для передачі вручну створеного модуля M.
Як тепер виглядає код сабкомпонента?
FirstActivityComponent.java:
@FirstActivityScope
@Subcomponent(modules = FirstActivityModule.class)
public interface FirstActivityComponent extends ActivityComponent<FirstActivity> {
@Subcomponent.Builder
interface Builder extends ActivityComponentBuilder<FirstActivityComponent, FirstActivityModule> {
}
}
Використовуємо створені інтерфейси із зазначенням усіх необхідних Generic: для ActivityComponent, і для ActivityComponentBuilder.
У підсумку отримуємо сабкомпонент, який інджектить FirstActivity, і його білдер під час створення попросить від нас модуль FirstActivityModule.
З SecondActivityComponent все аналогічно:
@SecondActivityScope
@Subcomponent(modules = SecondActivityModule.class)
public interface SecondActivityComponent extends ActivityComponent<SecondActivity> {
@Subcomponent.Builder
interface Builder extends ActivityComponentBuilder<SecondActivityComponent, SecondActivityModule> {
}
}
Сабкомпонент, який інджектить SecondActivity, і його білдер під час створення попросить від нас модуль SecondActivityModule.
Дивимося AppComponent:
@AppScope
@Component(modules = AppModule.class)
public interface AppComponent {
void injectComponentsHolder(ComponentsHolder componentsHolder);
}
У ньому більше немає методів для створення сабкомпонентів. Але є метод, який інджектить ComponentsHolder. Цей метод передасть у ComponentsHolder білдери сабкомпонентів і з їхньою допомогою ComponentsHolder сам зможе створити сабкомпоненти.
Звідки AppComponent візьме білдери для сабкомпонентів, щоб заінджектити їх у ComponentsHolder? Відповідь міститься в модулі AppModule:
@Module(subcomponents = {FirstActivityComponent.class, SecondActivityComponent.class})
public class AppModule {
private final Context context;
public AppModule(Context context) {
this.context = context;
}
@AppScope
@Provides
Context provideContext() {
return context;
}
@Provides
@IntoMap
@ClassKey(FirstActivity.class)
ActivityComponentBuilder provideFirstActivityBuilder(FirstActivityComponent.Builder builder) {
return builder;
}
@Provides
@IntoMap
@ClassKey(SecondActivity.class)
ActivityComponentBuilder provideSecondActivityBuilder(SecondActivityComponent.Builder builder) {
return builder;
}
}
У анотації @Module є аргумент subcomponents (доступний з версії 2.7). У ньому ми можемо вказати сабкомпоненти, які містять білдери @Subcomponent.Builder. Тобто білдери, які ми самі описали. І тепер модуль вміє створювати ці білдери.
У нашому прикладі ми прописали
@Module(subcomponents = {FirstActivityComponent.class, SecondActivityComponent.class})
Це означає, що тепер цей модуль знає, як створити об'єкти FirstActivityComponent.Builder і SecondActivityComponent.Builder. А відповідно це знає і AppComponent, який використовує цей модуль. І AppComponent може заінжектити ці білдери в ComponentsHolder, який буде їх використовувати, щоб створити сабкомпоненти FirstActivityComponent і SecondActivityComponent.
Але, щоб зробити цю схему красивішою й універсальнішою, довелося її трохи ускладнити. Замість того, щоб інджектити в ComponentsHolder окремі білдери, ми збиратимемо їх у Map, і його вже передаватимемо в ComponentsHolder.
Тобто це буде Map<Class<?>, ActivityComponentBuilder>. Як ключ будемо використовувати клас Activity, а як значення - загальний інтерфейс для білдерів.
Метод provideFirstActivityBuilder помістить у Map запис із ключем FirstActivity.class і з білдером FirstActivityComponent.Builder як значення. Метод provideSecondActivityBuilder помістить у Map запис із ключем SecondActivity.class і з білдером SecondActivityComponent.Builder як значення. І цей Map буде передано в ComponentsHolder.
Подивимося, що відбуватиметься в ComponentsHolder:
public class ComponentsHolder {
private final Context context;
@Inject
Map<Class<?>, Provider<ActivityComponentBuilder>> builders;
private Map<Class<?>, ActivityComponent> components;
private AppComponent appComponent;
public ComponentsHolder(Context context) {
this.context = context;
}
void init() {
appComponent = DaggerAppComponent.builder().appModule(new AppModule(context)).build();
appComponent.injectComponentsHolder(this);
components = new HashMap<>();
}
public AppComponent getAppComponent() {
return appComponent;
}
public ActivityComponent getActivityComponent(Class<?> cls) {
return getActivityComponent(cls, null);
}
public ActivityComponent getActivityComponent(Class<?> cls, ActivityModule module) {
ActivityComponent component = components.get(cls);
if (component == null) {
ActivityComponentBuilder builder = builders.get(cls).get();
if (module != null) {
builder.module(module);
}
component = builder.build();
components.put(cls, component);
}
return component;
}
public void releaseActivityComponent(Class<?> cls) {
components.put(cls, null);
}
}
У змінну Map<Class<?>, Provider> builders ми отримаємо Map з білдерами з AppComponent. Зверніть увагу, що в цьому Map ми використовуємо Provider. Тобто замість білдера, в Map передається провайдер, який вміє створювати білдер. Це зроблено для економії пам'яті. Потрібний нам білдер буде створюватися тільки коли він нам знадобиться. А без використання Provider ми б одразу отримали в Map усі білдери, і, найімовірніше, деякі з них жодного разу не були б використані під час роботи програми.
У Map<Class<?>, ActivityComponent> components зберігатимемо створювані сабкомпоненти, щоб під час повороту екрана ми могли знову надати їх Activity.
Метод getActivityComponent робить приблизно те саме, що й раніше - створює сабкомпонент, якщо він ще не був створений. Для цього використовується відповідний білдер з Map builders, а як ключ ми будемо використовувати клас Activity. Якщо при створенні компонента було передано модуль, то він буде використаний у білдері.
Метод releaseActivityComponent обнуляє компонент у Map components, щоб під час наступного запиту він був створений заново.
За такої реалізації, нам не треба додавати сюди однакові для кожного сабкомпонента методи get і release. Щойно ми пропишемо новий сабкомпонент в AppModule, він додасться сюди автоматично.
Подивимося код Activity
SecondActivity.java:
public class SecondActivity extends AppCompatActivity {
public static final String EXTRA_ARGS = "args";
@Inject
SecondActivityPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_activity);
Bundle args = getIntent().getBundleExtra(EXTRA_ARGS);
SecondActivityComponent component =
(SecondActivityComponent) App.getApp(this).getComponentsHolder()
.getActivityComponent(getClass(), new SecondActivityModule(args));
component.inject(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (isFinishing()) {
App.getApp(this).getComponentsHolder().releaseActivityComponent(getClass());
}
}
}
Використовуючи getClass() ми отримуємо компонент для цього Activity і приводимо його до типу SecondActivityComponent. Якщо необхідно, ми можемо створити і використовувати модуль SecondActivityModule.
Метод обнулення також використовує getClass, щоб ComponentsHolder розумів, про який компонент ідеться.