Урок 5. Builder
30 марта 2017
В этом уроке мы подробно рассмотрим билдеры: как они генерируются даггером, как можно использовать свой билдер, как с помощью аннотации @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. Она доступна начиная с версии 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
Там созданы две ветки:
subcomponents_old - старый способ создания сабкомпонентов, с помощью метода create в родительском компоненте
subcomponents_builder - новый способ создания сабкомпонентов с помощью своего билдера
Общая схема приложения одинакова для обоих веток. Вкратце расскажу о ней, чтобы легче было понимать код.
В приложении есть два экрана: 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.
C 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, а в качестве значения - общий интерфейс для билдеров.
Подробно о Map вы можете прочитать в Уроке 2.
Метод 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 понимал о каком компоненте речь.