Dagger2 - 如何在运行时有条件地选择模块

时间:2018-02-19 21:29:02

标签: android dagger-2

我有一个大型的Android应用程序需要运行不同的代码,具体取决于操作系统版本,制造商和许多其他东西。然而,这个应用程序需要是一个APK。它需要在运行时足够聪明才能确定要使用的代码。到目前为止,我们一直在使用Guice,但性能问题导致我们考虑迁移到Dagger。但是,我一直无法确定我们是否能够实现相同的用例。

主要目标是让我们在启动时运行一些代码,以提供兼容模块的列表。然后将此列表传递给Dagger以连接所有内容。

以下是我们要迁移的Guice中当前实现的一些伪代码

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())
    }
  }  
}

所以在启动时应用程序执行此操作:

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

其中,crazyBusinessLogic()读取所有模块的注释,并根据设备属性确定要用于每个功能的单个模块。例如:

  • 一台OS = 5.0的三星设备将拥有crazyBusinessLogic()返回列表{new WifiSamsung44Module(),new NfcDefaultModule()}
  • 一台OS = 7.0的三星设备将拥有crazyBusinessLogic()返回列表{new WifiSamsung44Module(),new Nfc60Module()}
  • OS = 7.0的Nexus设备将使用crazyBusinessLogic()返回列表{new Wifi44Module(),new Nfc60Module()}
  • 依旧......

有没有办法对Dagger做同样的事情? Dagger似乎要求您传递Component注释中的模块列表。

我读了一个似乎可以在一个小型演示上工作的博客,但它似乎很笨重,而且组件的额外if语句和额外接口可能会导致我的代码膨胀。

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

有没有办法只使用我们在Guice中执行的函数返回的模块列表?如果不是,那么最小化重写注释和crazyBusinessLogic()方法的最接近的方法是什么?

2 个答案:

答案 0 :(得分:6)

Dagger在编译时生成代码,因此您不会像在Guice中那样拥有足够的模块灵活性;而不是Guice能够反射性地发现@Provides方法并运行反射configure()方法,Dagger将需要知道如何创建它在运行时可能需要的每个实现,并且它需要在编译时知道 。因此,无法传递任意数组的模块并让Dagger正确连接图表;它击败了Dagger编写的编译时检查和性能。

也就是说,对于包含所有可能实现的单个APK,您似乎没问题,因此唯一的问题是在运行时在它们之间进行选择。这在Dagger中是非常可能的,并且可能属于以下四种解决方案之一:David的基于组件依赖性的解决方案,模块子类,有状态模块实例或基于@BindsInstance的重定向。

组件依赖

David's blog you linked一样,您可以定义一个接口,其中包含一组需要传入的绑定,然后通过传递给构建器的接口实现提供这些绑定。虽然接口的结构使得设计良好,可以将Dagger @Component实现传递给其他Dagger @Component实现,但接口可以由任何实现。

但是,我不确定此解决方案是否适合您:此结构也适用于继承独立实现,而不是在您的各种WifiManager实现都具有图形所需的依赖项的情况下满足。如果您需要支持&#34;插件,可能会被这种类型的解决方案所吸引。架构,或者如果您的Dagger图形太大以至于单个图形不应包含应用程序中的所有类,但除非您有这些约束,否则您可能会发现此解决方案冗长且具有限制性。

模块子类

Dagger允许非final模块,并允许将实例传递到模块中,因此您可以通过将模块的子类传递到Builder中来模拟您拥有的方法。你的组件。因为替换/覆盖实现的能力经常与测试相关联,所以在Dagger 2 Testing page under the heading "Option 1: Override bindings by subclassing modules (don’t do this!)"中对此进行了描述 - 它清楚地描述了这种方法的注意事项,特别是虚拟方法调用将比静态{{1}慢。 }方法,并且任何被覆盖的@Provides方法都必须采用任何实现所使用的所有参数。

@Provides

这是有效的,因为您可以提供单个模块实例并将其视为abstract factory pattern,但通过不必要地调用// 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(); ,您并未充分利用Dagger。此外,需要维护所有可能的依赖项的完整列表可能会使它比它的价值更麻烦,特别是考虑到您希望所有依赖项都在同一个APK中发布。 (如果您需要某些类型的插件架构,或者您希望避免完全基于编译时标志或条件发布实现,这可能是一个更轻量级的替代方案。)

模块实例

提供可能虚拟模块的能力对于使用构造函数参数传递模块实例更有意义,然后您可以在实现之间进行选择。

new

同样,这并没有充分利用Dagger的潜力;您可以通过手动委派给您想要的正确的提供商来实现这一目标。

// 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();

更好!现在你不创建任何实例,除非你需要它们,Nfc60Manager和NfcDefaultManager可以获取Dagger提供的任意参数。这导致了第四种解决方案:

注入配置

// 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();
  }
}

通过这种方式,您可以将业务逻辑封装在自己的配置对象中,让Dagger提供所需的方法,然后返回使用静态// 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(); 的抽象模块以获得最佳性能。此外,您不需要为您的API使用Dagger @Module实例,这会隐藏实现细节,并且如果您的需求发生变化,可以更轻松地从Dagger中移除。对于您的情况,我推荐这个解决方案;它会进行一些重组,但我认为你最终会有一个更清晰的结构。

关于Guice模块#configure(Binder)

的附注

调用@Provides并不是惯用的;请改用install(feature);。这样,Guice可以更好地描述代码中出现错误的位置,在模块中发现feature.configure(binder())方法,并在模块安装多次的情况下对模块实例进行重复数据删除。

答案 1 :(得分:1)

  

有没有办法只使用从a返回的模块列表   功能就像我们在Guice做的那样?如果不是,最接近的是什么   最小化重写注释和方法的方法   crazyBusinessLogic()方法?

不确定这是您正在寻找的答案,但是如果您有其他选择,我会描述完全不同的方法。

我想说你到目前为止使用Guice的方式是滥用DI框架,你最好利用这个机会消除这种滥用,而不是在Dagger中实现它。

让我解释一下。

依赖注入架构模式的主要目标是将构造逻辑与功能逻辑分开。

您基本上想要实现的是标准多态 - 根据一组参数提供不同的实现。

如果您为此目的使用模块和组件,您将最终根据管理这些多态实现需求的业务规则来构建您的DI代码。

这种方法不仅需要更多的样板,而且还可以防止出现具有有意义结构的内聚模块,并提供对应用程序设计和架构的深入了解。

此外,我怀疑您是否能够在依赖注入逻辑内“编码”这些业务规则。

有两种方法更好恕我们。

第一种方法仍然不是很干净,但至少它不会损害依赖注入代码的大规模结构:

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

在实现之间选择的逻辑仍然存在于DI代码中,但至少它没有进入该部分的大规模结构。

但在这种情况下,最好的解决方案是进行适当的面向对象设计,而不是滥用DI框架。

我很确定所有这些类的源代码非常相似。他们甚至可以在重写一种方法的同时继承彼此。

在这种情况下,正确的方法不是重复/继承,而是使用策略设计模式的组合。

您可以将“策略”部分提取到类的独立层次结构中,并定义一个工厂类,该工厂类根据系统的参数构造它们。然后,你可以这样做:

@Provides
WiFiStrategyFactory wiFiStrategyFactory(DeviceInfoProvider deviceInfoProvider) {
    return new WiFiStrategyFactory(deviceInfoProvider);
}

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

现在构建逻辑简单明了。封装在WiFiStrategyFactory内的策略之间的区别可以进行单元测试。

这种正确方法的最佳部分是,当需要实施新策略时(因为我们都知道Android碎片是不可预测的),您不需要实现新的模块和组件,也不需要对其进行任何更改DI结构。这个新要求将通过提供策略的另一个实现并将实例化逻辑添加到工厂来处理。

通过单元测试保证安全。