В данной статье рассмотрены особенности применения мульбайндинга, который может помочь решить множество проблем связанных с предоставлением зависимостей.
Для данной статьи необходимы базовые знания по 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