Dagger2 - How to conditionally select modules at runtime

I have a BIG Android application that should run different code depending on the OS version, manufacturer, and many other things. However, this application must be one APK. It must be smart enough at runtime to determine which code to use. So far we have used Guice, but performance issues are forcing us to consider switching to the Dagger. However, I was not able to determine whether we can achieve the same use case.

The main goal is that we have code that runs at startup to provide a list of compatible modules. Then pass this list to the dagger to tie everything together.

Here are some pseudo-codes of the current implementation in Guice that we want to transfer

import com.google.inject.AbstractModule; @Feature("Wifi") public class WifiDefaultModule extends AbstractModule { @Override protected void configure() { bind(WifiManager.class).to(WifiDefaultManager.class); bind(WifiProcessor.class).to(WifiDefaultProcessor.class); } } @Feature("Wifi") @CompatibleWithMinOS(OS > 4.4) class Wifi44Module extends WifiDefaultModule { @Override protected void configure() { bind(WifiManager.class).to(Wifi44Manager.class); bindProcessor(); } @Override protected void bindProcessor() { (WifiProcessor.class).to(Wifi44Processor.class); } } @Feature("Wifi") @CompatibleWithMinOS(OS > 4.4) @CompatibleWithManufacturer("samsung") class WifiSamsung44Module extends Wifi44Module { @Override protected void bindProcessor() { bind(WifiProcessor.class).to(SamsungWifiProcessor.class); } @Feature("NFC") public class NfcDefaultModule extends AbstractModule { @Override protected void configure() { bind(NfcManager.class).to(NfcDefaultManager.class); } } @Feature("NFC") @CompatibleWithMinOS(OS > 6.0) class Nfc60Module extends NfcDefaultModule { @Override protected void configure() { bind(NfcManager.class).to(Nfc60Manager.class); } } public interface WifiManager { //bunch of methods to implement } public interface WifiProcessor { //bunch of methods to implement } public interface NfcManager { //bunch of methods to implement } public class SuperModule extends AbstractModule { private final List<Module> chosenModules = new ArrayList<Module>(); public void addModules(List<Module> features) { chosenModules.addAll(features); } @Override protected void configure() { for (Module feature: chosenModules) { feature.configure(binder()) } } } 

therefore, at startup, the application does this:

 SuperModule superModule = new SuperModule(); superModule.addModules(crazyBusinessLogic()); Injector injector = Guice.createInjector(Stage.PRODUCTION, superModule); 

where crazyBusinessLogic () reads the annotations of all modules and determines one that will be used for each function based on the properties of the device. For example:

  • a Samsung device with OS = 5.0 will have crazyBusinessLogic () to return the list {new WifiSamsung44Module (), the new NfcDefaultModule ()}
  • a Samsung device with OS = 7.0 will have crazyBusinessLogic () return a list {new WifiSamsung44Module (), new Nfc60Module ()}
  • A Nexus device with OS = 7.0 will have crazyBusinessLogic () to return the list {new Wifi44Module (), the new Nfc60Module ()}
  • etc....

Is there a way to do the same with a dagger? It seems that the dagger requires you to pass the list of modules in component annotations.

I read a blog that seems to be working on a little demo, but it seems awkward, and the extra if statement and additional interfaces for the components can trigger my code.

https://blog.davidmedenjak.com/android/2017/04/28/dagger-providing-different-implementations.html

Is there a way to use the list of modules returned by the function, for example, what we do in Guice? If not, what would be the closest way to minimize the rewriting of annotations and the crazyBusinessLogic () method?

+9
android dagger-2
source share
2 answers

The dagger generates code at compile time, so you will not have the flexibility of a module like in Guice; instead of Guice being able to reflectively detect @Provides methods and run the configure() reflexive method, Dagger will need to know how to create every implementation that may be needed at runtime, and it will need to know what is at compile time. Therefore, there is no way to pass an arbitrary array of Modules and have Dagger correctly running your schedule ; he defeats the runtime check and performance that the Dagger wrote to ensure.

However, you seem to be all right with one APK containing all possible implementations, so the only thing is to choose between them at runtime. This is very possible in a dagger and will likely fall into one of four solutions: a solution for David's dependency component -based, subclasses of modules, instances of state modules or @BindsInstance -based.

Component dependencies

Like David’s blog with which you are associated , you can define an interface with a set of bindings that you need to pass in, and then provide these bindings through the implementation of this interface passed to the constructor. Although the interface structure makes it well designed to implement Dagger @Component implementations in other Dagger @Component implementations, the interface can be implemented in any way.

However, I'm not sure if this solution suits you: this structure is also best suited for inheriting standalone implementations, and not in your case when your various WifiManager implementations have dependencies that your schedule should satisfy. You can turn to this type of solution if you need to support a "plug-in" architecture or if your Dagger graph is so huge that one graph does not have to contain all the classes in your application, but if you do not have these restrictions you can find this solution detailed and restrictive.

Subclasses of Modules

The dagger allows non- final modules and allows you to transfer instances to modules, so you can mimic the approach you have by passing subclasses of your modules into the Builder of your component. Since the possibility of replacing / overriding implementations is often associated with testing, this is described on the Dagger 2 testing page under the heading “Option 1: redefine bindings with subclass modules (don't do this!)” - this clearly describes the caveats of this approach, in particular, that calling the virtual method will be slower than the static @Provides method, and that any overridden @Provides methods will have to accept all the parameters that any implementation uses.

 // Your base Module @Module public class WifiModule { @Provides WifiManager provideWifiManager(Dep1 dep1, Dep2 dep2) { /* abstract would be better, but abstract methods usually power * @Binds, @BindsOptionalOf, and other declarative methods, so * Dagger doesn't allow abstract @Provides methods. */ throw new UnsupportedOperationException(); } } // Your Samsung Wifi module @Module public class SamsungWifiModule { @Override WifiManager provideWifiManager(Dep1 dep1, Dep2 dep2) { return new SamsungWifiManager(dep1); // Dep2 unused } } // Your Huawei Wifi module @Module public class HuaweiWifiModule { @Override WifiManager provideWifiManager(Dep1 dep1, Dep2 dep2) { return new HuaweiWifiManager(dep1, dep2); } } // To create your Component YourAppComponent component = YourAppComponent.builder() .baseWifiModule(new SamsungWifiModule()) // or name it anything // via @Component.Builder .build(); 

This works as you can put one instance of a module and consider it as an abstract factory pattern , but by calling new unnecessarily, you are not using Dagger to its full potential. Also, having to maintain a complete list of all possible dependencies can make it more of a problem than it costs, especially considering that you want all the dependencies to be sent in one APK. (This may be an alternative with lighter weight if you need certain types of plugin architecture, or you want to avoid delivering the implementation entirely based on flags or compilation conditions).

Module Examples

The ability to provide a possibly-virtual module really meant more to pass instances of modules with constructor arguments, which can then be used to choose between implementations.

 // Your NFC module @Module public class NfcModule { private final boolean useNfc60; public NfcModule(boolean useNfc60) { this.useNfc60 = useNfc60; } @Override NfcManager provideNfcManager() { if (useNfc60) { return new Nfc60Manager(); } return new NfcDefaultManager(); } } // To create your Component YourAppComponent component = YourAppComponent.builder() .nfcModule(new NfcModule(true)) // again, customize with @Component.Builder .build(); 

Again, this does not use the dagger in full; You can do this by manually delegating the provider you need.

 // Your NFC module @Module public class NfcModule { private final boolean useNfc60; public NfcModule(boolean useNfc60) { this.useNfc60 = useNfc60; } @Override NfcManager provideNfcManager( Provider<Nfc60Manager> nfc60Provider, Provider<NfcDefaultManager> nfcDefaultProvider) { if (useNfc60) { return nfc60Provider.get(); } return nfcDefaultProvider.get(); } } 

It's better! Now you do not create any instances if you do not need them, and Nfc60Manager and NfcDefaultManager can take arbitrary parameters that Dagger provides. This leads to a fourth solution:

Paste Configuration

 // Your NFC module @Module public abstract class NfcModule { @Provides static NfcManager provideNfcManager( YourConfiguration yourConfiguration, Provider<Nfc60Manager> nfc60Provider, Provider<NfcDefaultManager> nfcDefaultProvider) { if (yourConfiguration.useNfc60()) { return nfc60Provider.get(); } return nfcDefaultProvider.get(); } } // To create your Component YourAppComponent component = YourAppComponent.builder() // Use @Component.Builder and @BindsInstance to make this easy .yourConfiguration(getConfigFromBusinessLogic()) .build(); 

This way, you can encapsulate your business logic in your own configuration object, let the dagger provide the necessary methods, and return to abstract modules with static @Provides for better performance. In addition, you do not need to use Dagger @Module instances for your API, which hides implementation details and makes it easier to switch from a dagger later if your needs change. For your case, I recommend this solution; this will require some restructuring, but I think you will have a clearer structure.

Guice Module # configure (Binder) Side Note

This is not idiomatic for calling the function.configure feature.configure(binder()) function; use install(feature); instead. This allows Guice to better describe where errors occur in your code, detect @Provides methods in your modules, and @Provides instances of your module in case the module is installed more than once.

+6
source share

Is it possible to use the list of modules returned with how we do in Guice? If not, what will be the closest way that minimizes the rewriting of annotations and the crazyBusinessLogic () method?

Not sure if this is the answer you are looking for, but just in case you have other options for other members of the community. I will describe a completely different approach.

I would say that the way you used Guice so far is abuse of the DI card, and it would be much better for you to use this feature to remove this abuse, and not to implement it in a dagger.

Let me explain.

The main goal of the injection injection architecture is to keep the construction logic separate from the functional logic.

What you basically want to achieve — standard polymorphism — provides various implementations based on a set of parameters.

If you use modules and components for this purpose, you will ultimately structure your DI code in accordance with the business rules governing the need for these polymorphic implementations.

This approach not only requires a much larger number of templates, but also prevents cohesive modules from having a meaningful structure and providing an understanding of application design and architecture.

In addition, I doubt that you will be able to unit test these business rules within the dependency injection logic.

There are two approaches that are much better IMHO.

The first approach is still not very clean, but at least it does not compromise the scale structure of the dependency injection code:

 @Provides WifiManager wifiManager(DeviceInfoProvider deviceInfoProvider) { if (deviceInfoProvider.isPostKitKat() ) { if (deviceInfoProvider.isSamsung()) { return new WifiMinagerSamsungPostKitKat(); } else { return new WifiMinagerPostKitKat(); } } else { return new WifiMinagerPreKitKat(); } } 

The logic that chooses between the implementation is still in the DI code, but at least it did not fall into the large-scale structure of this part.

But the best solution in this case is to create a proper object-oriented design, and not to abuse the DI framework.

I am sure that the source code of all these classes is very similar. They can even inherit from each other, redefining only one method.

In this case, the correct approach is not duplication / inheritance, but composition using the strategy strategy template.

You would extract part of the “strategy” into an autonomous class hierarchy and define a factory class that will build them based on system parameters. Then you can do it like this:

 @Provides WiFiStrategyFactory wiFiStrategyFactory(DeviceInfoProvider deviceInfoProvider) { return new WiFiStrategyFactory(deviceInfoProvider); } @Provides WifiManager wifiManager(WiFiStrategyFactory wiFiStrategyFactory) { return new WifiMinager(WiFiStrategyFactory.newWiFiStrategy()); } 

Now the construction logic is simple and straightforward. The difference between the strategies concluded inside WiFiStrategyFactory , and can be checked per unit.

The best part of this correct approach is that when a new strategy is needed (since we all know that Android fragmentation is unpredictable), you will not need to introduce new modules and components or make any changes to the DI structure. This new requirement will be addressed by providing yet another implementation of the strategy and adding creation logic to the factory.

All this while maintaining safety with unit tests.

+1
source share

All Articles