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