SubComponent та Scope

У цьому уроці розглянемо, що таке 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.

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

Додаток являє собою найпростіший поштовий клієнт із трьома екранами.

  1. Логін Залогінившись, потрапляємо на такий екран
  2. Список папок

При натисканні на папку відкриється наступний екран

  1. Список листів

Додаток працює на заглушках, тобто реальна робота з сервером там не реалізована. Мета прикладу - показати, як можна використовувати дагер. Тому все інше скорочено по максимуму, щоб вийшло якомога менше коду. Повороти екрана не обробляються.

У цьому додатку задіяно 5 компонентів:

  • AppComponent - створюється на весь час роботи програми. Відповідно, об'єкти, які він уміє створювати і які мають той самий scope, що й у нього, будуть синглтонами протягом життя цього компонента. У цьому прикладі - це клас по роботі з мережею ApiService.
  • MailComponent - створюється на час роботи з поштою. Його синглтон - це клас для роботи з поштою MailManager. І по одному компоненту для кожного Activity. Їхні синглтони - це презентери.

Розглянемо такий сценарій роботи програми.

Йдемо екранами зліва направо. На екрані логіна вводимо ім'я користувача і пароль, переходимо на список його папок, відкриваємо одну з них і бачимо листи з цієї папки.

На схемі можна наочно побачити час життя кожного компонента:

  • AppComponent існує весь час роботи програми, тобто на всіх екранах.
  • MailComponent ми створюємо щойно у нас починається робота з поштою користувача. Цей компонент використовується на екранах папок і листів.
  • і для кожного Activity створюється свій компонент на час життя цього Activity.

Трохи розширимо сценарій

З екрана листів ми повертаємося на екран папок, а потім на екран логіна. У цьому разі MailComponent нам більше не потрібен. А якщо ми знову вирішуємо залогінитися і попрацювати з поштою, створюється новий MailComponent.

Якщо у вас є бажання подивитися на реальні робочі приклади реалізації дагера, то їх можна знайти в інтернеті. Від себе можу порекомендувати ось цей приклад.