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

Pau*_*gas 10 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())
    }
  }  
}
Run Code Online (Sandbox Code Playgroud)

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

SuperModule superModule = new SuperModule();
superModule.addModules(crazyBusinessLogic());
Injector injector = Guice.createInjector(Stage.PRODUCTION, superModule);
Run Code Online (Sandbox Code Playgroud)

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

  • OS = 5.0的三星设备将使用crazyBusinessLogic()返回列表{new WifiSamsung44Module(),new NfcDefaultModule()}
  • OS = 7.0的三星设备将使用crazyBusinessLogic()返回列表{new WifiSamsung44Module(),new Nfc60Module()}
  • OS = 7.0的Nexus设备将返回list {new Wifi44Module(),new Nfc60Module()}
  • 等等....

对Dagger有什么办法吗?Dagger似乎要求您传递Component注释中的模块列表.

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

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

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

Jef*_*ica 6

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

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

组件依赖性

您链接的David博客中,您可以定义一个接口,其中包含您需要传递的一组绑定,然后通过传递给构建器的该接口的实现提供这些绑定.虽然接口的结构使得这个设计良好,可以将Dagger @Component实现传递给其他Dagger @Component实现,但是接口可以由任何东西实现.

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

模块子类

Dagger允许非final模块,并允许将实例传递到模块中,因此您可以通过将模块的子类传递到组件的构建器来模拟您拥有的方法.因为替换/覆盖实现的能力经常与测试相关联,所以在Dagger 2测试页面的标题"选项1:通过子类化模块覆盖绑定(不要这样做!)"标题下对此进行了描述 - 这清楚地描述了警告这种方法,特别是虚方法调用将比静态@Provides方法慢,并且任何重写的@Provides方法都必须采取任何实现使用的所有参数.

// 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();
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为您可以提供单个模块实例并将其视为抽象工厂模式,但通过new不必要地调用,您不会充分发挥Dagger的潜力.此外,需要维护所有可能的依赖项的完整列表可能会使它比它的价值更麻烦,特别是考虑到您希望所有依赖项都在相同的APK中发布.(如果您需要某些类型的插件架构,或者您希望避免完全基于编译时标志或条件运送实现,这可能是一个更轻量级的替代方案.)

模块实例

提供可能虚拟模块的能力实际上更适用于使用构造函数参数传递模块实例,然后您可以使用它们在实现之间进行选择.

// 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();
Run Code Online (Sandbox Code Playgroud)

同样,这并没有充分发挥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();
  }
}
Run Code Online (Sandbox Code Playgroud)

更好!现在你不创建任何实例,除非你需要它们,Nfc60Manager和NfcDefaultManager可以获取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();
Run Code Online (Sandbox Code Playgroud)

这样,您可以将业务逻辑封装在自己的配置对象中,让Dagger提供所需的方法,然后返回使用static抽象模块以@Provides获得最佳性能.此外,您不需要为您的API使用Dagger @Module实例,这会隐藏实现细节,并且如果您的需求发生变化,可以更轻松地从Dagger中移开.对于您的情况,我推荐这个解决方案; 它需要一些重组,但我认为你最终会有一个更清晰的结构.

关于Guice模块的附注#configure(Binder)

打电话不是惯用的feature.configure(binder()); 请install(feature);改用.这使Guice能够更好地描述代码中出现错误的位置,发现@Provides模块中的方法,以及在模块多次安装时去除模块实例的重复数据.