This article describes the features of the use of multi-binding, which can help solve many problems associated with the provision of dependencies.
This article requires basic knowledge of Dagger 2. The examples used are Dagger version 2.11.
Dagger 2 allows you to bind several objects into a collection, even in cases where the binding of these objects occurs in different modules. Dagger 2 supports Set
and Map
multi-siding.
To add an element to the Set
, it is enough to add the annotation of the @IntoSet
over the @Provides
method in the module:
@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); } }
Add these two modules to our component:
@Component(modules = {ModuleA.class, ModuleB.class}) public interface AppComponent { //inject methods }
Since we have combined our two modules, which contain the binding of the elements into a set into one component, the dagger will combine these elements into one collection:
public class Values { @Inject public Values(Set<FileExporter> values) { // values: [XmlFileExporter, CSVFileExporter] } }
We can also add several elements at a time, for this we need our @Provide
method to have a return type Set
and set the @ElementsIntoSet
annotation over the @Provide
method.
Replace our ModuleB:
@Module public class ModuleB { @ElementsIntoSet @Provides public Set<FileExporter> provideFileExporters(Context context) { return new HashSet<>(Arrays.asList(new CSVFileExporter(context), new JSONFileExporter(context))); } }
Result:
public class Values { @Inject public Values(Set<FileExporter> values) { // values: [XmlFileExporter, CSVExporter, JSONFileExporter] } }
You can provide dependency through the component:
@Component(modules = {ModuleA.class, ModuleB.class}) public interface AppComponent { Set<FileExporter> fileExporters(); } Set<FileExporter> fileExporters = DaggerAppComponent .builder() .context(this) .build() .fileExporters();
We can also provide collections using the @Qualifier
over the @Provides
method, thereby separating them.
Replace our ModuleB again:
@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 provides the ability to delay the initialization of objects until the first call, and this option is also available for collections. In the arsenal of Dagger 2, there are two ways to achieve delayed initialization: using the interfaces Provider<T> Lazy<T>
.
For any dependency on T
, you can use Lazy<T>
, this method allows you to delay initialization until the first call to Lazy<T>.get()
. If T
singleton, then the same instance will always be returned. If T
unscope, then the T
dependency will be created at the time of calling Lazy<T>.get
and placed in the cache inside Lazy<T>
and each subsequent call of this particular Lazy<T>.get()
will return the cached value.
Example:
@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(); } }
Instances groupRepository1, groupRepository2
and groupRepository3
will be equal, because they have a singleton skoup.
The userRepository1
and userRepository2
will be equal, because When the userRepositoryInstance1.get()
was first accessed, an object was created and placed in the cache inside userRepositoryInstance1
, but userRepository3
will have a different instance, since he has another Lazy
and for the first time get()
was called.
Provider<T>
also allows you to delay the initialization of objects, but unlike Lazy<T>
, unscope dependencies are not cached in Provider<T>
and return a new instance every time. Such an approach may be needed, for example, when we have a certain factory with a singleton crowd and this factory should provide new objects each time, consider an example:
@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(); } }
Here we have holder1
and holder2
would have different instances, if we used Lazy<T>
instead of Provider<T>
we would have these objects have one instance due to caching.
Delayed initialization can be applied to Set
:Lazy<Set<T>> Provider<Set<T>>
, cannot be used like this: Set<Lazy<T>>
.
public class MainActivity extends AppCompatActivity { @Inject Lazy<Set<FileExporter>> fileExporters; //… // Set<FileExporter> exporters = fileExporters.get(); }
In order to add an element to the Map
, it is necessary to add the @IntoMap
annotation and the key annotation (Heirs @MapKey
) over the @Provides
method in the module:
@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 }
Result:
public class Values { @Inject public Values(Map<String, FileExporter> values) { // values {xml=XmlFileExporter,csv=CSVExporter} } }
As with Set
, we specified two of our modules in the component, so Dagger merged our values ​​into a single Map
. You can also use @Qualifier
.
Standard Key Types for Map
:
Standard types of keys for the Dagger-Android add-on module:
How does the implementation look like for example ActivityKey
:
@MapKey @Target(METHOD) public @interface ActivityKey { Class<? extends Activity> value(); }
You can create your own key types, as described above or, for example, with 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} } }
As with Set
we can use lazy initialization:Lazy<Map<K,T>>, Provider<Map<K,T>>
.
With Map, we can use deferred not only initialization of the collection itself, but initialization of a separate element and receive a new value by key each time ( 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
and fileExporter2
will have different instances. And the Exports.XML
element is Exports.XML
even initialized, since we have not addressed him.
We cannot use Map<K, Lazy<T>>
.
To zaprovaydit an empty collection, we need to add the annotation @Multibinds
over the abstract method:
@Module public abstract class AppModule { @Multibinds abstract Map<Exporters, FileExporter> exporters(); }
This may be necessary, for example, when we already want to use this collection, but the module with implementations is not yet available (not implemented), and when the module is implemented and added, it will merge the values ​​into a common collection.
The parent component has collections available only in the modules of the parent component, and the subcomponent “inherits” all the collections of the parent component and combines them with the subcomponent collections:
@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(); }
Dagger 2 allows you to bind objects to the collection using abstract @Binds methods:
@Module public abstract class LocationTrackerModule { @Binds @IntoSet public abstract LocationTracker netLocationTracker(NetworkLocationTracker tracker); @Binds @IntoSet public abstract LocationTracker fileLocationTracker(FileLocationTracker tracker); }
To build plugin-architected applications, we use the dedendency framework to separate the interfaces from the implementation, so the “Plugin” can be reused in various applications:
With the help of Multibindings we can create an interface and a method that will be an extension point for many plugins:
In my opinion, Multibindings provides quite wide possibilities for organizing the provision of dependencies, we can beautifully organize our factories, and is also suitable for implementing an expansion architecture.
GitHub example
Source: https://habr.com/ru/post/336414/
All Articles