[startandroid] Dagger 2 - Урок 2

Урок 2. Дополнительные возможности

03 октября 2016

В первом уроке мы изучили, как компонент создает и возвращает нам объекты. Во втором уроке рассмотрим некоторые дополнительные возможности: 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 - ThreadHandlerUi
ThreadHandlerType.DB - ThreadHandlerDb

Inject

Напоследок немного про аннотацию Inject. Напомню, что мы использовали ее для того, чтобы пометить переменные в Activity. В этом случае при вызове inject-метода, компонент заполнит эти переменные объектами.

Но есть еще пара применений, которые могут вам пригодится.

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

  2. Также этой аннотацией можно пометить методы Activity. И когда компонент будет инджектить это Activity, он после заполнения полей вызовет эти методы.

1 симпатия