[startandroid] Dagger 2 - Урок 3

Урок 3. SubComponent и Scope

06 октября 2016

В этом уроке рассмотрим, что такое SubComponent и как задается время жизни объектов с помощью Scope.

SubComponent

Сабкомпоненты - это просто компоненты, которые наследуют и расширяют родительский компонент. Т.е. кроме объектов в своих модулях, они видят и все объекты из модулей родительского компонента.

Сабкомпоненты описываются так же, как и компоненты, но аннотацией Subcomponent

@Subcomponent(modules={MailModule.class})
public interface MailComponent {
 
}

Метод создания сабкомпонента описывается в родительском компоненте

@Component(modules = {AppModule.class})
public interface AppComponent {
 
    MailComponent createMailComponent();
 
}


А реализация метода остается за даггером. Теперь, вызвав у компонента AppComponent метод createMailComponent вы получите сабкомпонент MailComponent, который умеет предоставлять свои объекты (из MailModule) и объекты родительского компонента (из AppModule)

Передача объектов в конструктор модуля

Как мы уже знаем, компоненту для создания объектов требуются модули. Они перечисляются в списке modules.

@Component(modules = {AppModule.class})
public interface AppComponent {
    ...
}

Экземпляры модулей, при этом, создаются внутри компонента. Для этого используются дефолтные конструкторы. Но в некоторых случаях может возникнуть необходимость передать извне какой-либо объект в модуль при его создании. Например, модуль AppModule требует для своей работы объект SomeObject

@Module
public class AppModule {
 
    public AppModule(SomeObject someObject) {
        ...
    }
}

В этом случае компонент не сможет самостоятельно создать модуль. И при создании компонента нам необходимо самим создать экземпляр модуля и передать его компоненту. Для этого в билдере компонента существует специальный метод под каждый модуль

component = DaggerAppComponent.builder().
        appModule(new AppModule(new SomeObject())).
        build();

В случае, когда модуль используется сабкомпонентом, схема будет немного другая. Ведь сабкомпонент создается родительским компонентом и мы не имеем доступ к билдеру. Чтобы передать модуль сабкомпоненту, нам необходимо указать этот модуль как параметр в методе создания сабкомпонента

@Component(modules = {AppModule.class})
public interface AppComponent {
 
    MailComponent createMailComponent(MailModule mailModule);
 
}

И при вызове передать экземпляр модуля

App.getComponent().createMailComponent(new MailModule(new SomeObject()));

Scope

По умолчанию, когда мы запрашиваем у компонента какой-либо объект, компонент каждый раз создает нам новый экземпляр этого объекта. Но мы можем изменить это поведение на singleton. Для этого используется scope аннотация.

Дальнейшее повествование не будет простым. Поэтому рекомендую прочесть его несколько раз, а еще лучше - попробовать на примерах.

Рассмотрим на примере. Мы хотим, чтобы AppComponent всегда возвращал нам один и тот же экземпляр DatabaseHelper.

Указываем scope аннотацию @Singleton для компонента

@Singleton
@Component(modules = {StorageModule.class, NetworkModule.class})
public interface AppComponent {
    NetworkUtils getNetworkUtils();
    DatabaseHelper getDatabaseHelper();
}

И эту же аннотацию указываем в модуле для provide метода, который используется компонентом для получения объекта DatabaseHelper.

@Module
public class StorageModule {
 
    @Singleton
    @Provides
    public DatabaseHelper provideDatabaseHelper() {
        return new DatabaseHelper();
    }
 
}

Компонент AppComponent предоставляет объект DatabaseHelper, и они оба помечены scope аннотацией Singleton. Это приведет к тому, что компонент будет хранить в себе синглтон объекта DatabaseHelper. И при каждом вызове метода getDatabaseHelper(), AppComponent будет возвращать этот синглтон. И т.к. AppComponent обычно живет все время работы приложения, то и DatabaseHelper, получаемый от компонента, у нас будет в одном экземпляре все время жизни приложения. В любом Activity, фрагменте, сервисе и т.д. при вызове метода AppComponent.getDatabaseHelper (или при инджекте) мы будем получать один и тот же экземпляр объекта DatabaseHelper. Это удобно использовать при создании объектов для работы с сетью, БД, файлами и т.п. Обычно эти объекты нужны нам в одном экземпляре на все приложение.

А метод getNetworkUtils() будет работать, как и раньше. Потому что у него нет той же scope аннотации, что и у компонента. При каждом вызове этого метода AppComponent будет создавать и возвращать новый NetworkUtils объект.

Т.е. ключевой момент при создании синглтона: provide метод объекта и компонент, предоставляющий этот объект, должны быть помечены одной и той же аннотацией.

По умолчанию нам предлагается использовать scope аннотацию, которая называется Singleton. Но мы можем сами создавать scope аннотации и давать им свои имена. Эти аннотации будут работать точно также, как и Singleton. Т.е. они будут делать объекты синглтонами на время жизни компонента, предоставляющего эти объекты. Профит своих аннотаций в том, что мы можем давать им более информативные имена, чем обезличенный “Singleton”.

Возвращаясь к примеру выше. AppComponent живет обычно все время работы приложения. И синглтоны, которые он нам предоставит, будут жить вместе с ним все время работы приложения. Поэтому для AppComponent вполне логично создать и использовать scope аннотацию, которая называется PerApplication.

Создаем свою аннотацию и называем ее PerApplication:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerApplication {
}

Давайте заменим Singleton на PerApplication.

В компоненте

@PerApplication
@Component(modules = {StorageModule.class, NetworkModule.class})
public interface AppComponent {
    NetworkUtils getNetworkUtils();
    DatabaseHelper getDatabaseHelper();
}

И в модуле

@Module
public class StorageModule {
 
    @PerApplication
    @Provides
    public DatabaseHelper provideDatabaseHelper() {
        return new DatabaseHelper();
    }
 
}

Работать это будет так же, как и с аннотацией Singleton: AppComponent предоставит нам синглтон DatabaseHelper. Но теперь открыв модуль StorageModule и увидев, что DatabaseHelper помечен PerApplication, мы понимаем, что этот объект у нас живет все время работы приложения. Т.е. эти имена полезны в первую очередь нам самим, чтобы взглянув на provide метод объекта мы сразу понимали его время жизни.

Рассмотрим еще пример. Есть некое UserActivity. В нем есть пара фрагментов: UserListFragment - для отображения списка пользователей, и UserDetailsFragment - для отображения инфы по выбранному из списка пользователю. Эти фрагменты для доступа к данным используют некий UserRepository.

Мы можем описать компонент UserComponent, который будет инджектить объекты в UserActivity, UserListFragment и UserDetailsFragment. Этот компонент будем создавать во время создания UserActivity. Соответственно, его время жизни будет равно времени жизни UserActivity, а следовательно оно покроет и время жизни фрагментов.

Можно создать scope аннотацию PerUserActivity, и пометить ей компонент UserComponent и объект UserRepository, который предоставляется этим компонентом. Теперь компонент все свое время жизни будет держать синглтон UserRepository. И т.к. этот компонент будет инджектить объекты в фрагменты, то оба фрагмента будут работать с одним и тем же экземпляром UserRepository. А мы, взглянув на код создания UserRepository в модуле, сразу увидим, что этот объект имеет то же время жизни, что и UserActivity.

По поводу времени жизни компонента. Как и кем оно определяется? Ответ - вами. Если вам нужно, чтобы компонент жил все время работы приложения - вы создаете его в Applciation.onCreate и в Application классе храните этот компонент. Если вам нужно, чтобы время жизни компонента было равно времени жизни Activity, вы создаете этот компонент в Activity.onCreate и в Activity классе храните этот компонент. Когда Activity закроется, компонент будет также уничтожен сборщиком мусора. Открыв это же Activity в следующий раз, вы создадите новый экземпляр этого компонента.

Соответственно и scope аннотации - это не какая-то магия. Это просто указание компоненту, чтобы он объект держал как singleton, а не создавал новый экземпляр при каждом нашем запросе. И время жизни этого singleton объекта будет равно времени жизни компонента. Создав новый компонент, вы получите новый singleton.

Я создал небольшой пример, в котором вы можете посмотреть как могут быть организованы компоненты и модули в приложении.

Приложение представляет из себя простейший почтовый клиент с тремя экранами.

  1. Логин

Залогинившись, попадаем на следующий экран

  1. Список папок

При нажатии на папку откроется следующий экран

  1. Список писем

Приложение работает на заглушках, т.е. реальная работа с сервером там не реализована. Цель примера - показать, как можно использовать даггер. Поэтому все остальное сокращено по максимуму, чтобы получилось как можно меньше кода. Повороты экрана не обрабатываются.

В этом приложении задействовано 5 компонентов:
AppComponent - создается на все время работы приложения. Соответственно, объекты, которые он умеет создавать и которые имеют тот же scope, что и у него, будут синглтонами на протяжении жизни этого компонента. В данном примере - это класс по работе с сетью ApiService.
MailComponent - создается на время работы с почтой. Его синглтон - это класс для работы с почтой MailManager.
И по одному компоненту для каждого Activity. Их синглтоны - это презентеры.

Рассмотрим такой сценарий работы приложения

Идем по экранам слева направо. На экране логина вводим имя пользователя и пароль, переходим на список его папок, открываем одну из них и видим письма из этой папки.

На схеме можно наглядно увидеть время жизни каждого компонента:

  • AppComponent существует все время работы приложения, т.е. на всех экранах.
  • MailComponent мы создаем как только у нас начинается работа с почтой пользователя. Этот компонент используется на экранах папок и писем.
  • и для каждого Activity создается свой компонент на время жизни этого Activity.

Немного расширим сценарий

С экрана писем мы возвращаемся на экран папок, а затем на экран логина. В этом случае MailComponent нам больше не нужен. А если мы снова решаем залогиниться и поработать с почтой, создается новый MailComponent.

Если у вас есть желание посмотреть на реальные рабочие примеры реализации даггера, то их можно найти в интернете. От себя могу порекомендовать вот этот пример.