Додаткові можливості Dagger 2

У першому уроці ми вивчили, як компонент створює і повертає нам об'єкти. У другому уроці розглянемо деякі додаткові можливості: Lazy, Provider, Named, Qualifier, Intoset, ElementsIntoSet, IntoMap, Inject.

Lazy

Ліниве створення об'єкта. Компонент надає не сам об'єкт, а провайдер, який створить об'єкт тільки при виклику методу get().

public class MainActivity extends Activity {
 
    @Inject
    Lazy<DatabaseUtils> mDatabaseUtilsProvider; // provider
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        App.getComponent().injectsMainActivity(this); 
        ...
        mDatabaseUtilsProvider.get(); // creates and returns DatabaseUtils object
    }
}

При виклику injectsMainActivity компонент не буде створювати об'єкт DatabaseUtils. Замість цього він поверне нам провайдер. Коли нам знадобиться об'єкт DatabaseUtils, ми викликаємо у провайдера метод get. Тільки в цей момент провайдер створює і повертає цей об'єкт. Усі наступні виклики get повертатимуть один і той самий об'єкт.

Provider

Аналогічний Lazy, але при кожному виклику get створює новий об'єкт.

Named

Розглянемо приклад коду

@Module
public class StorageModule {
 
    @Provides
    public DatabaseUtils provideDatabaseUtils() {
        return new DatabaseUtils("database.db");
    }
 
    @Provides
    public DatabaseUtils provideDatabaseUtilsTest() {
        return new DatabaseUtils("test.db");
    }
 
}

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

Анотація Named вирішує цю проблему.

@Module
public class StorageModule {
 
    @Named("prod")
    @Provides
    public DatabaseUtils provideDatabaseUtils() {
        return new DatabaseUtils("database.db");
    }
 
    @Named("test")
    @Provides
    public DatabaseUtils provideDatabaseUtilsTest() {
        return new DatabaseUtils("test.db");
    }
 
}

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

В Activity у випадку інджекта:

@Named("prod")
@Inject
DatabaseUtils mDatabaseUtils;
 
@Named("test")
@Inject
DatabaseUtils mDatabaseUtilsTest;

У компоненті, у разі get-методів:

@Named("prod")
DatabaseUtils getDatabaseUtils();
 
@Named("test")
DatabaseUtils getDatabaseUtilsTest();

Компонент по типу объекта и тексту аннотации Named найдет нужный объект и вернет вам его.

Qualifier

Ми можемо створювати свої анотації та використовувати їх замість щойно розглянутого нами @Named. Створимо дві анотації: DatabaseProd і DatabaseTest. Для цього треба просто створити два наступні класи:

DatabaseProd.java

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface DatabaseProd {
}

DatabaseTest.java

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface DatabaseTest {
}

Або можна описати їх усередині вже готового класу. Тут як вам зручніше.

Ми створили наші власні анотації і тепер можемо використовувати їх.

У модулі:

@Module
public class StorageModule {
 
    @DatabaseProd
    @Provides
    public DatabaseUtils provideDatabaseUtils() {
        return new DatabaseUtils("database.db");
    }
 
    @DatabaseTest
    @Provides
    public DatabaseUtils provideDatabaseUtilsTest() {
        return new DatabaseUtils("test.db");
    }
}

В Activity під час інджекту:

@DatabaseProd
@Inject
DatabaseUtils mDatabaseUtils;
 
@DatabaseTest
@Inject
DatabaseUtils mDatabaseUtilsTest;

У компоненті, в get-методах:

@DatabaseProd
DatabaseUtils getDatabaseUtils();
 
@DatabaseTest
DatabaseUtils getDatabaseUtilsTest();

Компонент за типом об'єкта й анотації знайде потрібний об'єкт і поверне вам його.

IntoSet

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

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

Нехай у нас є два обробники.

public class AnalyticsManager implements EventHandler {
    //...
}
public class Logger implements EventHandler {
    //...
}

І ми хочемо отримати їх від дагера відразу в один Set.

Для цього ми в модулі використовуємо анотацію @intoSet для методів, які повертають ці об'єкти

@Module
public class EventModule {
 
    @Provides
    @IntoSet
    EventHandler provideAnalyticsManager() {
        return new AnalyticsManager();
    }
 
    @Provides
    @IntoSet
    EventHandler provideLogger() {
        return new Logger();
    }
}

А в Activity описуємо Set

@Inject
Set<EventHandler> eventHandlers;

Під час інджекту компонент створить об'єкти AnalyticsManager і Logger і помістить у цей Set, оскільки вони є об'єктами типу EventHandler.

ElementsIntoSet

Анотацію IntoSet ми застосовували, коли модуль створював EventHandler і ми хотіли, щоб він потрапив у наш набір Set. Але може бути випадок, коли модуль повертає не один об'єкт, а набір об'єктів, тобто Set. І нам необхідно, щоб усі об'єкти з цього Set потрапили в наш Set. У цьому випадку необхідно використовувати анотацію ElementsIntoSet.

@Module
public class EventModule {
 
    @Provides
    @ElementsIntoSet
    Set<EventHandler> provideHandlers() {
        return new HashSet<>(Arrays.asList(new AnalyticsManager(), new Logger()));
    }
     
}

Компонент візьме всі об'єкти з цього набору і помістить у потрібний вам набір в Activity.

Щоб зібрати один набір об'єктів, можна використовувати і @IntoSet, і @ElementsIntoSet. Я розглянув їх окремо тільки для спрощення.

Якщо вам потрібно розподілити кілька однотипних об'єктів по різних колекціях, ви можете використовувати @Named або @Qualifier анотації.

IntoMap

Аналогічний IntoSet. Компонент зможе зібрати для нас об'єкти в Map. Відмінність у тому, що нам треба буде для кожного об'єкта вказати ключ, з яким цей об'єкт буде поміщений у Map.

Наприклад, ми хочемо використовувати кілька ThreadHandler і для їх зберігання будемо використовувати Map<String, ThreadHandler>. Ключем буде рядок, що описує призначення ThreadHandler, наприклад, “UI” і “DB”.

Код у модулі:

@Module
public class ThreadModule {
 
    @Provides
    @IntoMap
    @StringKey("UI")
    ThreadHandler provideThreadHandlerUi() {
        return new ThreadHandlerUi();
    }
 
    @Provides
    @IntoMap
    @StringKey("DB")
    ThreadHandler provideThreadHandlerDb() {
        return new ThreadHandlerDb();
    }    
     
}

Анотація IntoMap означає, що об'єкт призначений для поміщення в Map. Анотацією StringKey ми задаємо одночасно і сам ключ, і його тип. У нашому випадку тип ключа в Map це String. Тип методів, що повертається, - це тип значення в Map.

В Activity описуємо map:

@Inject
Map<String, ThreadHandler> threadHandlerMap;

І під час інджекту компонент заповнить цей Map парами (ключ - значення):

  • "UI" - ThreadHandlerUi
  • "DB" - ThreadHandlerDb

Даггер за замовчуванням надає анотації для завдання ключів типу String, Long, Integer і Class. За необхідності можна створювати свої анотації і вказувати там свій тип. Наприклад, замість String ми можемо використовувати свій enum ThreadHandlerType.

Описуємо enum

enum ThreadHandlerType {
    UI, DB
}

Створюємо анотацію, що описує тип ключа

@MapKey
public @interface ThreadHandlerTypeKey {
    ThreadHandlerType value();
}

Застосування цієї анотації означатиме, що ключ об'єкта має тип ThreadHandlerType.

Використовуємо її в Provide методах замість StringKey

@Module
public class ThreadModule {
 
    @Provides
    @IntoMap
    @ThreadHandlerTypeKey(ThreadHandlerType.UI)
    ThreadHandler provideThreadHandlerUi() {
        return new ThreadHandlerUi();
    }
 
    @Provides
    @IntoMap
    @ThreadHandlerTypeKey(ThreadHandlerType.DB)
    ThreadHandler provideThreadHandlerDb() {
        return new ThreadHandlerDb();
    }
 
}

Якщо для анотації StringKey ми вказували рядки, то в нашій створеній анотації ми вказуємо об'єкти ThreadHandlerType.

Map в Activity буде таким:

@Inject
Map<ThreadHandlerType, ThreadHandler> threadHandlerMap;

І під час інджекту компонент заповнить цей Map парами (ключ - значення): ThreadHandlerType.UI - ThreadHandlerUiThreadHandlerType.DB - ThreadHandlerDb

Inject

Наостанок трохи про анотацію Inject. Нагадаю, що ми використовували її для того, щоб позначити змінні в Activity. У цьому разі при виклику inject-методу, компонент заповнить ці змінні об'єктами.

Але є ще пара застосувань, які можуть стати вам у пригоді.

Якщо один із конструкторів класу позначений цією анотацією, то компонент використовує цей конструктор, щоб створити об'єкт без участі модулів.

Також цією анотацією можна позначити методи Activity. І коли компонент буде інджектити це Activity, він після заповнення полів викличе ці методи.