В данной статье рассмотрены особенности применения мульбайндинга, который может помочь решить множество проблем связанных с предоставлением зависимостей.
Для данной статьи необходимы базовые знания по Dagger 2. В примерах использовался Dagger версии 2.11
Dagger 2 позволяет забайндить несколько объектов в коллекцию, даже в тех случаях, когда байндинг этих объектов происходит в разных модулях. Dagger 2 поддерживает Set и Map мультибайндинг.
Set multibindings
Для того чтобы добавить элемент в Set, достаточно добавить аннотацию @IntoSet над @Provides методом в модуле:
@Module
public class ModuleA {
@IntoSet
@Provides
public FileExporter xmlFileExporter(Context context) {
return new XmlFileExporter(context);
}
}
@Module
public class ModuleB {
@IntoSet
@Provides
public FileExporter provideCSVFileExporter(Context context) {
return new CSVFileExporter(context);
}
}Добавим два этих модуля в наш компонент:
@Component(modules = {ModuleA.class, ModuleB.class})
public interface AppComponent {
//inject methods
}Т.к. мы объединили наши два модуля, которые содержат байндинг элементов в сет в один компонент, даггер объединит эти элементы в одну коллекцию:
public class Values {
@Inject
public Values(Set<FileExporter> values) {
//значения в values: [XmlFileExporter, CSVFileExporter]
}
}Мы также можем добавить несколько элементов за один раз, для этого нам надо, чтобы наш @Provide метод имел возвращаемый тип Set и поставить аннотацию @ElementsIntoSet над @Provide методом.
Заменим наш ModuleB:
@Module
public class ModuleB {
@ElementsIntoSet
@Provides
public Set<FileExporter> provideFileExporters(Context context) {
return new HashSet<>(Arrays.asList(new CSVFileExporter(context),
new JSONFileExporter(context)));
}
}Результат:
public class Values {
@Inject
public Values(Set<FileExporter> values) {
//значения в values: [XmlFileExporter, CSVExporter, JSONFileExporter]
}
}Можно предоставлять зависимость через компонент:
@Component(modules = {ModuleA.class, ModuleB.class})
public interface AppComponent {
Set<FileExporter> fileExporters();
}
Set<FileExporter> fileExporters = DaggerAppComponent
.builder()
.context(this)
.build()
.fileExporters();Также мы можем предоставлять коллекции с использованием @Qualifier над @Provides методом, тем самым разделять их.
Заменим еще раз наш ModuleB:
@Module
public class ModuleB {
@ElementsIntoSet
@Provides
@Named("CSV_JSON")
public Set<FileExporter> provideFileExporters(Context context) {
return new HashSet<>(Arrays.asList(new CSVFileExporter(context),
new JSONFileExporter(context)));
}
}
// Без Qualifier
public class Values {
@Inject
public Values(Set<FileExporter> values) {
//значения в values: [XmlFileExporter].
//Здесь мы указали без кваливайра, поэтому
//будут собраны объекты c ModuleA.
}
}
// С Qualifier
public class Values {
@Inject
public Values(@Named("CSV_JSON") Set<FileExporter> values) {
//значения в values: [CSVExporter, JSONFileExporter]
}
}
//Через компонент
@Component(modules = {ModuleA.class, ModuleB.class})
public interface AppComponent {
@Named("CSV_JSON") Set<FileExporter> fileExporters();
}
Dagger 2 предоставляет возможность отложить инициализацию объектов до первого вызова, и эта возможность есть и для коллекций. В арсенале Dagger 2 есть два способа для достижения отложенной инициализации: с использованием интерфейсов Provider<T> и Lazy<T>.
Lazy injections
Для любой зависимости T, вы можете применить Lazy<T>, данный способ позволяет отложить инициализацию до первого вызова Lazy<T>.get(). Если T синглтон, то будет возвращаться всегда один и тот же экземпляр. Если же T unscope, тогда зависимость T будет создана в момент вызова Lazy<T>.get и помещена в кэш внутри Lazy<T> и каждый последующий вызов именно этого Lazy<T>.get(), будет возвращать кэшированное значение.
Пример:
@Module
public class AppModule {
@Singleton
@Provides
public GroupRepository groupRepository(Context context) {
return new GroupRepositoryImpl(context);
}
@Provides
return new UserRepositoryImpl(context);
public UserRepository userRepository(Context context) {
}
}
public class MainActivity extends AppCompatActivity {
@Inject
Lazy<GroupRepository> groupRepositoryInstance1;
@Inject
Lazy<GroupRepository> groupRepositoryInstance2;
@Inject
Lazy<UserRepository> userRepositoryInstance1;
@Inject
Lazy<UserRepository> userRepositoryInstance2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerAppComponent
.builder()
.context(this)
.build()
.inject(this);
//GroupRepository @Singleton scope
GroupRepository groupRepository1 = groupRepositoryInstance1.get();
GroupRepository groupRepository2 = groupRepositoryInstance1.get();
GroupRepository groupRepository3 = groupRepositoryInstance2.get();
//UserRepository unscope
UserRepository userRepository1 = userRepositoryInstance1.get();
UserRepository userRepository2 = userRepositoryInstance1.get();
UserRepository userRepository3 = userRepositoryInstance2.get();
}
}Инстансы groupRepository1, groupRepository2 и groupRepository3 будут равны, т.к. они имет скоуп синглтон.
Инстансы userRepository1 и userRepository2 будут равны, т.к. при первом обращении к userRepositoryInstance1.get() был создан объект и помещен в кэш внутри userRepositoryInstance1, а вот userRepository3 будет иметь другой инстанс, т.к. он имеет другой Lazy и для него был вызван первый раз get().
Provider injections
Provider<T> также позволяет отложить инициализацию объектов, но в отличии от Lazy<T>, значения unscope зависимостей не кэшируется в Provider<T> и возвращают каждый раз новый инстанс. Такой подход может понадобится к примеру когда у нас есть некая фабрика со скопом синглтон и эта фабрика должна предоставлять каждый раз новые объекты, рассмотрим пример:
@Module
public class AppModule {
@Provides
public Holder provideHolder() {
return new Holder();
}
@Provides
@Singleton
public HolderFactory provideHolderFactory(Provider<Holder> holder) {
return new HolderFactoryImpl(holder);
}
}
public class HolderFactoryImpl implements HolderFactory {
private Provider<Holder> holder;
public HolderFactoryImpl(Provider<Holder> holder) {
this.holder = holder;
}
public Holder create() {
return holder.get();
}
}
public class MainActivity extends AppCompatActivity {
@Inject
HolderFactory holderFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerAppComponent
.builder()
.context(this)
.build()
.inject(this);
Holder holder1 = holderFactory.create();
Holder holder2 = holderFactory.create();
}
}Здесь у нас holder1 и holder2 будут иметь разные инстансы, если бы мы использовали бы Lazy<T> вместо Provider<T> у нас бы эти объекты имели бы один инстанс из за кэширования.
Отложенную инициализацию можно применить и к Set:
Lazy<Set<T>> или Provider<Set<T>>, нельзя использовать так: Set<Lazy<T>>.
public class MainActivity extends AppCompatActivity {
@Inject
Lazy<Set<FileExporter>> fileExporters;
//…
// Set<FileExporter> exporters = fileExporters.get();
}Map multibindings
Для того чтобы добавить элемент в Map, необходимо добавить аннотацию @IntoMap и аннотацию ключа (Наследники @MapKey) над @Provides методом в модуле:
@Module
public class ModuleA {
@IntoMap
@Provides
@StringKey("xml")
public FileExporter xmlFileExporter(Context context) {
return new XmlFileExporter(context);
}
}
@Module
public class ModuleB {
@IntoMap
@StringKey("csv")
@Provides
public FileExporter provideCSVFileExporter(Context context) {
return new CSVFileExporter(context);
}
}
@Component(modules = {ModuleA.class, ModuleB.class})
public interface AppComponent {
//inject methods
}Результат:
public class Values {
@Inject
public Values(Map<String, FileExporter> values) {
//значения в values {xml=XmlFileExporter,csv=CSVExporter}
}
}Также как и с Set, мы указали два наших модуля в компоненте, таким образом Dagger объединил наши значения в единую Map. Также можно использовать @Qualifier.
Стандартные типы ключей для Map:
- IntKey
- LongKey
- StringKey
- ClassKey
Стандартные типы ключей дополнительного модуля Dagger-Android:
- ActivityKey
- BroadcastReceiverKey
- ContentProviderKey
- FragmentKey
- ServiceKey
Как выглядит реализация к примеру ActivityKey:
@MapKey
@Target(METHOD)
public @interface ActivityKey {
Class<? extends Activity> value();
}Можно создавать свои типы ключей, как выше описано или к примеру с enum:
public enum Exporters {
XML,
CSV
}
@MapKey
@Target(METHOD)
public @interface ExporterKey {
Exporters value();
}
@Module
public class ModuleA {
@IntoMap
@Provides
@ExporterKey(Exporters.XML)
public FileExporter xmlFileExporter(Context context) {
return new XmlFileExporter(context);
}
}
@Module
public class ModuleB {
@IntoMap
@ExporterKey(Exporters.CSV)
@Provides
public FileExporter provideCSVFileExporter(Context context) {
return new CSVFileExporter(context);
}
}
public class Values {
@Inject
public Values(Map<Exporters, FileExporter> values) {
//значения в values {XML=XmlFileExporter,CSV=CSVExporter}
}
}Как и с Set мы можем применять отложенную инициализацию:
Lazy<Map<K,T>>, Provider<Map<K,T>>.
С Map мы можем использовать отложенную не только инициализацию самой коллекции, но инициализацию отдельного элемента и получать по ключу каждый раз новое значение (Map<K,Provider<T>>):
public class MainActivity extends AppCompatActivity {
@Inject
Map<Exporters, Provider<FileExporter>> exporterMap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerAppComponent
.builder()
.context(this)
.build();
FileExporter fileExporter1 = exporterMap.get(Exporters.CSV).get();
FileExporter fileExporter2 = exporterMap.get(Exporters.CSV).get();
}
}fileExporter1 и fileExporter2 будут иметь разные инстансы. А элемент Exports.XML даже и не проинициализируется, т.к. мы к нему не обращались.
Мы не можем использовать Map<K, Lazy<T>>.
Чтобы запровайдить пустую коллекцию, нам необходимо добавить аннотацию @Multibinds над абстрактным методом:
@Module
public abstract class AppModule {
@Multibinds
abstract Map<Exporters, FileExporter> exporters();
}Это может понадобиться например когда мы хотим уже использовать эту коллекцию, но модуль с реализациями еще не доступен (не реализован), а когда модуль реализуем и добавим, он объединит значения в общую коллекцию.
Subcomponents и мультибайндинг
Родительскому компоненту доступны коллекции указанные только в модулях родительского компонента, а сабкомпонент “наследует” все коллекции родительского компонента и объединяет их с коллекциями сабкомпонента:
@Module
public class AppModule {
@IntoMap
@Provides
@ExporterKey(Exporters.XML)
public FileExporter xmlFileExporter(Context context) {
return new XmlFileExporter(context);
}
}
@Module
public class ActivityModule {
@IntoMap
@ExporterKey(Exporters.CSV)
@Provides
public FileExporter provideCSVFileExporter(Context context) {
return new CSVFileExporter(context);
}
}
@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
ActivitySubComponent provideActivitySubComponent();
//значения в коллекции {xml=XmlFileExporter}
Map<Exporters, FileExporter> exporters();
@Component.Builder
interface Builder {
@BindsInstance
Builder context(Context context);
AppComponent build();
}
}
@ActivityScope
@Subcomponent(modules = {ActivityModule.class})
public interface ActivitySubComponent {
//значения в коллекции {XML=XmlFileExporter,CSV=CSVExporter}
Map<Exporters, FileExporter> exporters();
}
@Binds + multibindings
Dagger 2 позволяет забайндить объекты в коллекцию с использованием абстрактных @Binds методов:
@Module
public abstract class LocationTrackerModule {
@Binds
@IntoSet
public abstract LocationTracker netLocationTracker(NetworkLocationTracker
tracker);
@Binds
@IntoSet
public abstract LocationTracker fileLocationTracker(FileLocationTracker
tracker);
}Plugins
Для построения plugin-architected приложения, мы используем депенденси инджекшн фреймворк для того чтобы разделить интерфейсы от реализации, таким образом “Plugin” может быть повторно использован в различных приложениях:

С помощью Multibindings мы можем создать интерфейс и провайд метод, который будет являться точкой расширения для множества плагинов:

Вывод
По моему мнению Multibindings предоставляет достаточно широкие возможности для организации предоставления зависимостей, мы можем красиво организовать наши фабрики, а также подходит для реализации архитектуры расширения.
Пример на GitHub
Sie
Dagger2 — отличный инструмен, спаибо за опсание еще одной удобной возможности.