DI 框架 Dagger2 系统性学习 - 不容错过的干货

1,169 阅读28分钟
原文链接: www.jianshu.com

Dagger2

转载请注明原作者,如果你觉得这篇文章对你有帮助或启发,可以关注打赏。

前言
本文翻译自Google Dagger2文档,才疏学浅,欢迎拍砖,希望能帮到你。

架构方面请关注GitHub(MVP+Retrofit+Dagger2+Okhttp)及我的文章Android UI框架快速搭建实践
Dagger2原理分析请关注Dagger2详解-从代码分析其原理

友情提示,因为简书markdown页内跳转支持问题,目录及其他页内跳转位置点击后会在浏览器打开新的tab,且不能跳转到相应位置,希望不会对你造成困扰。

目录

Home
User's Guide
Android
Multibinding Sets and Maps
Subcomponents
Producers
Testing
Project Pages

到底部

Home

Dagger是一个完全静态的编译时的Java和Android依赖注入矿建。它由Square发布的早期版本改造而来现在由Google维护。

Dagger致力于解决开发中使用基于反射的的解决方案带来的性能问题。更多详情可以在这里找到 by +Gregory Kick.

文档

代码

有问题?

User's Guide

应用中好的类是那些做了事情实现了功能的类,例如:BarcodeDecoder、KoopaPhysicsEngine和AudioStreamer。这些类可能依赖了其他类,如:BarcodeCameraFinder、DefaultPhysicsEngine及HttpStreamer。

相比之下,应用中那些糟糕的类往往占用空间却没做什么事,例如:BarcodeDecoderFactory、CameraServiceLoader以及MutableContextWrapper,这些类就像胶带一样笨拙地将相关的东西捆在一起。

实现了依赖注入设计模式而且不需要书写模板的Dagger是这些工厂类的替代品。它可以让你专注于你感兴趣的类。声明依赖,然后指定怎么注入它们,然后发布你的应用。

构建基于标准javax.inject注解(JSR 330),类的测试更容易。你不需要写一大堆样板文件只需要用FakeCreditCardService替换RpcCreditCardService即可。

依赖注入不仅仅应用于测试。它还便于创建通用的,可复用的模块。你可以在你所有的应用共享一个AuthenticationModule。你还可以在开发环境中运行DevLoggingModule,在生产环境中运行ProdLoggingModule以在不同情景下都能达到正确的行为。

Dagger2的亮点

依赖注入的框架已经存在好多年了并且拥有多种多样的API来配置和注入。那为什么要重新造轮子呢?Dagger2是第一个使用生成的代码实现全栈的依赖注入框架。指导原则是模仿用户手写的代码来生成代码尽可能地保证依赖注入过程简单、可追踪、高性能。想了解更多关于这种设计的信息请观看视频(幻灯片) by +Gregory Kick

Using Dagger

我们将通过建造一个咖啡机的过程来演示依赖注入和Dagger.可编译运行的完整样例代码请看Dagger的咖啡样例

声明依赖

Dagger构建你的应用中的实例并满足他们的依赖。它使用javax.inject.Inject注解来标识感兴趣的构造器和字段。

使用@Inject注解告诉Dagger创建类的实例应该用的构造器。当需要一个实例对象时,Dagger将会获得需要的参数并调用这个构造器。

class Thermosiphon implements Pump {
  private final Heater heater;

  @Inject
  Thermosiphon(Heater heater) {
    this.heater = heater;
  }

  ...
}

Dagger 可以直接注入字段(成员变量)。在这个例子中,它拿到Heater类实例对象和Pump类实例对象分别赋值给CoffeeMaker的heater字段和pump字段。

class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;

  ...
}

如果你的类有使用@Inject注解的字段但没有@Inject注解了的构造器与之对应,Dagger会注入这些字段但不会创建新的实例。添加一个带有@Inject注解的无参构造器来告诉Dagger可以创建对象。

Dagger也支持方法注入,但更推荐构造器注入和字段注入。

缺少@Inject注解的类是不能被Dagger构建的。

实现依赖

默认情况下,Dagger会通过创建需要类型的实例来提供依赖。当你需要一个CoffeeMaker,它会通过new CoffeeMaker()来获得一个实例并赋值给需要注入的字段。

但@Inject不是哪都有效的:

  • 接口不能被创建(不支持接口注入)
  • 第三方的类不能被注解(第三方类没有@Inject注解,除非可以改源码)
  • 可配置的对象必须配置好(这个应该是泛型,本人在使用时发现注入是不支持泛型的)

在上面这些场景下@Inject就有些尴尬了,使用@Provides注解方法来实现依赖。方法的返回类型与其要实现的依赖一致。
例如:只要需要Heater实例就会调用provideHeater()方法。

@Provides static Heater provideHeater() {
  return new ElectricHeater();
}

@Provides标注的方法可以依赖他们自己。任何时候当需要Pump对象时,下面这个方法返回一个Thermosiphon对象。
所有的@Provides注解的方法必须属于一个Module.这些类只是有@Module注解的类。

@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater() {
    return new ElectricHeater();
  }

  @Provides static Pump providePump(Thermosiphon pump) {
    return pump;
  }
}

按照惯例,@Provides方法命名带provide前缀,module类命名带Module后缀。

建立对象图

@Inject和@Provides注解的类通过他们的依赖关系联系起来形成对象图。调用代码就像一个应用的main方法或者Android应用通过一组明确定义的根集访问那个对象图。Dagger2中,那个集合是由一个包含返回需要类型且无参的方法的接口定义。通过@Component注解这个接口并传入module类型的参数,Dagger2然后根据这个协议生成所有实现。

@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
  CoffeeMaker maker();
}

实现类的类名与接口的名字加上Dagger前缀相同。通过调用实现类的builder()方法可以获得Builder实例,通过这个实例可以设置依赖并build()得到一个新的实例。

CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
.dripCoffeeModule(new DripCoffeeModule())
.build();

Note:如果你的@Component不是顶层的类型,那生成的component的名字将会包含它的封闭类型的名字,通过下划线连接。例如:

class Foo {
      static class Bar {
        @Component
        interface BazComponent {}
      }
}

将会生成名为DaggerFoo_ Bar_BazComponent的component.

任何有可达的默认构造器的module都可以被省略,如果没有设置builder会自动创建一个实例。而且任何@Provides方法都是静态的module,builder是不需要其实例的。如果不需要用户创建依赖实例就可以创建所有的依赖,那么生成的实现类将会包含一个create()方法,可以使用此方法得到一个实例而不用于builder打交道。

CoffeeShop coffeeShop = DaggerCoffeeShop.create();

现在,我们的CoffeeApp可以方便的通过Dagger生成的实现来得到一个完全注入的CoffeeMaker.

public class CoffeeApp {
  public static void main(String[] args) {
    CoffeeShop coffeeShop = DaggerCoffeeShop.create();
    coffeeShop.maker().brew();
  }
}

现在图已经建立了入口也已经注入了,我们开启我们的咖啡机应用。

$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[_]P coffee! [_]P

对象图中的绑定

上面的例子展现如何构建一个拥有一些典型绑定的component,但还有不同的机制来为图贡献绑定。作为依赖下面这些是可用的而且可以用来生成更好的component.

  • 这些@Module中由@Provides声明的方法可以直接被@Component.modules或@Module.includes引用
  • 任何类型的@Inject注解的构造器可以没有作用域也可以有与某个Component作用域一致的@Scope注解
  • component依赖component提供方法
  • component自己
  • 任何包含的subcomponent的不合格的builders
  • 上面所有绑定的Provider和Lazy 包装器。
  • 上面绑定的懒加载的供应器(e.g Provider>)
  • 任何类型的MemberInjector

单例和域绑定

@Singleton注解@Provide方法或可注入的类,对象图将会在应用中使用同一个一个实例。

@Provides @Singleton static Heater provideHeater() {
    return new ElectricHeater();
}

可注入的类上的@Singleton注解也可以作为文档。它告诉潜在的维护者这个类可能被多个线程共享。

@Singleton
class CoffeeMaker {
    ...
}

因为Dagger2会将图中添加了作用域的实例和component实现类的实例联系起来,所以这些component需要声明作用域。例如:在同一个component中使用@Singleton和@RequestScoped是没有意义的,因为他们具有不同的生命周期。想要声明一个具有作用域的component,只需要在该接口添加域注解。

@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
  CoffeeMaker maker();
}

Components可以有多种域注解。表明这些注解是同一个域的别名,这样component就可以包含它声明的域的所有绑定了。

可重用的scope

有时你可能想限制@Inject注解的构造器初始化的次数或者@Provides方法被调用的次数,但并不需要保证单例。这在内存比较吃紧的环境比如Android下会很有用。

当你使用@Resuable注解,这些@Resuable域绑定不像其他的域,不会和任何component联系,相反,每个使用这个绑定component会将返回值或初始化的对象缓存起来。

这意味着如果你在component中装载了@Resuable绑定的module,但只有一个子component使用了,那么那个子component将会缓存此绑定的对象。如果两个子component都使用了这个绑定但他们不继承同一个component,那么这两个子component的缓存是独立的。如果component已经缓存了对象,其子component会重用该对象。

并不能保证component只会调用该绑定一次,所以在返回可变对象或者需要使用单例的绑定上使用@Resuable是很危险的。对不关心被分配多少次的不变对象使用@Resuable是安全的。

@Reusable // 我们用了多少scopers并不重要,但不要浪费他们。
class CoffeeScooper {
  @Inject CoffeeScooper() {}
}

@Module
class CashRegisterModule {
  @Provides
  @Reusable // 不要这样做!你是关注你保存cash的register的
            // Use a specific scope instead.
  static CashRegister badIdeaCashRegister() {
    return new CashRegister();
  }
}


@Reusable // 不要这样做! 你实际想每次都拿到新的filter对象,所以这里不需要使用域。
class CoffeeFilter {
  @Inject CoffeeFilter() {}
}

延迟注入

有时你需要延迟初始化对象。对于任意绑定T,你可以创建Lazy,这样就可以延迟对象初始化直到调用Lazy的get()方法。如果T是单例的,那么在对象图中所有的注入都是同一个Lazy实例。否则每个注入拿到的都是自己的Lazy实例。对同一个Lazy实例连续调用get()方法返回的都是一个T对象。

class GridingCoffeeMaker {
  @Inject Lazy lazyGrinder;

  public void brew() {
    while (needsGrinding()) {
      //第一次调用get()时会创建Grinder对象并缓存起来
      lazyGrinder.get().grind();
    }
  }
}

Provider注入

有时你需要返回多个实例而不是注入单个值。你有多种选择(Factories,Builders,等等),其中一种选择就是注入一个Provider而不是T。每次调用get()方法时Provider会调用绑定逻辑。如果那个绑定逻辑是@Inject注解的构造器,会创建一个新对象,但一个@Provides方法是无法保证这点的。

class BigCoffeeMaker {
  @Inject Provider filterProvider;

  public void brew(int numberOfPots) {
  ...
    for (int p = 0; p < numberOfPots; p++) {
      maker.addFilter(filterProvider.get()); //每次都是新的filter对象          
      maker.addCoffee(...);
      maker.percolate();
      ...
    }
  }
}

Note:注入Provider可能降低代码的可读性。通常你会用一个factory或一个Lazy或是重新组织代码的结构和生命周期来注入一个T。但注入Provider有些情况可以救命。一个通常的使用场景就是当你必须使用一个遗留的并不与你的对象的自然生命周期一样的架构时。(例如:按照设计servlets是单例的,但只有在明确请求数据的上下文中中是有效的)。

Qualifiers

有时类型不足以区分依赖。例如:一个复杂的咖啡机想要将睡和盘子的加热器分开。

这种情况,我们添加一个qualifier annotation。这是任何本身有@Qualifier注解的注解。下面是@Named的声明,它是javax.inject中的注解。

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
  String value() default "";
}

你可以创建自定义的qualifier注解,或使用@Named.在关心的字段或参数上使用qualifiers.类型+qualifier将会用来标识一个依赖。

class ExpensiveCoffeeMaker {
  @Inject @Named("water") Heater waterHeater;
  @Inject @Named("hot plate") Heater hotPlateHeater;
  ...
}

注解对应的@Provides方法来提供限定的值。

@Provides @Named("hot plate") static Heater provideHotPlateHeater() {
  return new ElectricHeater(70);
}

@Provides @Named("water") static Heater provideWaterHeater() {
  return new ElectricHeater(93);
}

依赖可以有多个qualifier注解。

编译期校验

Dagger的注解处理器会生成名如CoffeeMaker_Factory.java或CoffeeMaker_MembersInjector.java的源文件。这些文件就是Dagger的实现细节。你不需要直接使用它们,虽然通过注解单步调试时会很方便。唯一需要你关心的是那些带有Dagger前缀的为component生成的代码。

Using Dagger In Your Build

Gradle Users

你需要引入运行时依赖dagger-2.2.jar,为了激活代码生成还要引入编译期依赖dagger-compiler-2.2.jar.

Maven工程在pom.xml如下配置:


  
    com.google.dagger
    dagger
    2.2
  
  
    com.google.dagger
    dagger-compiler
    2.2
    true
  

Android Gradle

// Add plugin https://bitbucket.org/hvisser/android-apt
buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

// Apply plugin
apply plugin: 'com.neenbedankt.android-apt'

// Add Dagger dependencies
dependencies {
  compile 'com.google.dagger:dagger:2.x'
  apt 'com.google.dagger:dagger-compiler:2.x'
}

Dagger&Android

相对于其他依赖注入框架Dagger2最基本的优势之一就是完全生成代码(没有反射),这意味着它是可以应用于Anroid的。但当使用时还是有一些需要注意的地方。

原理(Philosophy)

因为针对Android编写的代码是Java代码,所以在风格上常有很大区别,这种差异的存在以适应Android平台独特性能考虑。

为了生成通用且轻便的代码,Dagger基于ProGuard对编译后的字节码进行后期处理。这样Dagger可以产生在server和Android上都很自然的代码,使用不同的工具链是生成的字节码在两个环境下都能高效执行。此外,Dagger可以明确保证其生成的Java代码在ProGuard优化后是可编译。

多重绑定(Multibindings)

Dagger支持将多个对象绑定进一个集合即使这些对象已经绑定在不同的module中。

你可以使用多重绑定实现插件架构,例如:不同的module都可以贡献自己对插件接口的实现这样中央类就可以使用这些插件集合。或者你可以有多个module贡献各自的service providers,以名称为键保存在一个map中。

Set multibindings

在module的方法上添加@IntoSet注解,向一个可注入的多重绑定的set贡献元素。

@Module
class MyModuleA {
  @Provides @IntoSet
  static String provideOneString(DepA depA, DepB depB) {
    return "ABC";
  }
}

你还可以在返回值为集合的方法使用@ElementsIntoSet注解来同时贡献多个元素。

@Module
class MyModuleB {
  @Provides @ElementsIntoSet
  static Set provideSomeStrings(DepA depA, DepB depB) {
    return new HashSet(Arrays.asList("DEF", "GHI"));
  }
}

现在component的一个绑定可以依赖这个set了:

class Bar {
  @Inject Bar(Set strings) {
    assert strings.contains("ABC");
    assert strings.contains("DEF");
    assert strings.contains("GHI");
  }
}

component也可以提供这个set:

@Component(modules = {MyModuleA.class, MyModuleB.class})
interface MyComponent {
  Set strings();
}

@Test void testMyComponent() {
  MyComponent myComponent = DaggerMyComponent.create();
  assertThat(myComponent.strings()).containsExactly("ABC", "DEF", "GHI");
}

除了可以依赖多重绑定的Set,还可以依赖Provider>或者Lazy>,不可以依赖Set>.

给每个@Provides方法添加qualifier向限定的多重绑定set贡献元素。

@Module
class MyModuleC {
  @Provides @IntoSet
  @MyQualifier
  static Foo provideOneFoo(DepA depA, DepB depB) {
    return new Foo(depA, depB);
  }
}

@Module
class MyModuleD {
  @Provides
  static FooSetUser provideFooSetUser(@MyQualifier Set foos) { … }
}

Map multibindings

Dagger支持使用多重绑定向一个可注入的map贡献entry只要这个map的key在编译期是可见的。

在module中添加带有返回值的方法添加@IntoMap注解和指定key的自定义注解。为了向一个限定的多重绑定map贡献entry,需要给每个@IntoMap方法添加qualifier注解。

然后你就可以注入map(Map)本身或包含providers的map(Map>).当你不想一次初始化所有对象而是想一次只拿到一个值的时候或当你想在每次查询map时都拿到新的对象时后者更有用。

Simple map keys

对于那些键是string、Class或封装的原始类型的map,使用dagger.mapkeys中定义的标准注解.

@Module
class MyModule {
  @Provides @IntoMap
  @StringKey("foo")
  static Long provideFooValue() {
    return 100L;
  }

  @Provides @IntoMap
  @ClassKey(Thing.class)
  static String provideThingValue() {
    return "value for Thing";
  }
}

@Component(modules = MyModule.class)
interface MyComponent {
  Map longsByString();
  Map, String> stringsByClass();
}

@Test void testMyComponent() {
  MyComponent myComponent = DaggerMyComponent.create();
  assertThat(myComponent.longsByString().get("foo")).isEqualTo(100L);
  assertThat(myComponent.stringsByClass().get(Thing.class))
      .isEqualTo("value for Thing");
}

对于key是枚举或泛型化类的map,定义一个包含map的key类型成员的注解并添加@MapKey注解。

enum MyEnum {
  ABC, DEF;
}

@MapKey
@interface MyEnumKey {
  MyEnum value();
}

@MapKey
@interface MyNumberClassKey {
  Class value();
}

@Module
class MyModule {
  @Provides @IntoMap
  @MyEnumKey(MyEnum.ABC)
  static String provideABCValue() {
    return "value for ABC";
  }

  @Provides @IntoMap
  @MyNumberClassKey(BigDecimal.class)
  static String provideBigDecimalValue() {
    return "value for BigDecimal";
  }
}

@Component(modules = MyModule.class)
interface MyComponent {
  Map myEnumStringMap();
  Map, String> stringsByNumberClass();
}

@Test void testMyComponent() {
  MyComponent myComponent = DaggerMyComponent.create();
  assertThat(myComponent.myEnumStringMap().get(MyEnum.ABC)).isEqualTo("value for ABC");
  assertThat(myComponent.stringsByNumberClass.get(BigDecimal.class))
      .isEqualTo("value for BigDecimal");
}

你的自定义注解可以是任何名字,并且可以是任何正确的注解成员类型除了数组类型。

Complex map keys

如果用一个注解成员不足以描述你的map的key,你可以使用整个注解作为key,只把@MapKey的unwrapValue设置为false,在这种情况下,自定义注解也可以包含数组成员。

@MapKey(unwrapValue = false)
@interface MyKey {
  String name();
  Class implementingClass();
  int[] thresholds();
}

@Module
class MyModule {
  @Provides @IntoMap
  @MyKey(name = "abc", implementingClass = Abc.class, thresholds = {1, 5, 10})
  static String provideAbc1510Value() {
    return "foo";
  }
}

@Component(modules = MyModule.class)
interface MyComponent {
  Map myKeyStringMap();
}

使用@AutoAnnotation来创建注解实例。
如果map使用复杂key,那你可能需要在运行时创建@MapKey注解实例传给get(Object)方法。最简单的做法是使用@AutoAnnotation创建初始化注解实例的静态方法。更多细节查看@AutoAnnotation文档

class MyComponentTest {
  @Test void testMyComponent() {
    MyComponent myComponent = DaggerMyComponent.create();
    assertThat(myComponent.myKeyStringMap()
        .get(createMyKey("abc", Abc.class, new int[] {1, 5, 10}))
        .isEqualTo("foo");
  }

  @AutoAnnotation
  static MyKey createMyKey(String name, Class implementingClass, int[] thresholds) {
    return new AutoAnnotation_MyComponentTest_createMyKey(name, implementingClass, thresholds);
  }
}
Maps whose keys are not known at compile time

编译期map的key可以确定或可以用注解表示的Map多重绑定才有效。如果你的map不满足这些限制,那就不能创建多重绑定map,但你可以使用set multibindings 绑定对象然后再转换为非多重绑定的map

@Module
class MyModule {
  @Provides @IntoSet
  static Map.Entry entryOne(…) {
    Foo key = …;
    Bar value = …;
    return new SimpleImmutableEntry(key, value);
  }

  @Provides @IntoSet
  static Map.Entry entryTwo(…) {
    Foo key = …;
    Bar value = …;
    return new SimpleImmutableEntry(key, value);
  }
}

@Module
class MyMapModule {
  @Provides
  static Map fooBarMap(Set> entries) {
    Map fooBarMap = new LinkedHashMap<>(entries.size());
    for (Map.Entry entry : entries) {
      fooBarMap.put(entry.getKey(), entry.getValue());
    }
    return fooBarMap;
  }
}

注意这种方式就不会有Map>的自动绑定了。如果你想要providers map,就需要multibound set的Map.Entry对象中包含providers。然后你的非多重绑定的map就可以有Provider值了。

@Module
class MyModule {
  @Provides @IntoSet
  static Map.Entry> entry(
      Provider barSubclassProvider) {
    Foo key = …;
    return new SimpleImmutableEntry(key, barSubclassProvider);
  }
}

@Module
class MyProviderMapModule {
  @Provides
  static Map> fooBarProviderMap(
      Set>> entries) {
    return …;
  }
}

Declaring multibindings

你可以向module中返回set或map的方法添加@Multibinds注解来声明多重绑定的set或map.

对于至少有@IntoSet,@ElementsIntoSet,或@IntoMap绑定中一个的set或map没必要使用@Multibinds,但如果他们可能为空则必须要添加此声明。

@Module
abstract class MyModule {
  @Multibinds abstract Set aSet();
  @Multibinds @MyQualifier abstract Set aQualifiedSet();
  @Multibinds abstract Map aMap();
  @Multibinds @MyQualifier abstract Map aQualifiedMap();
}

给定的set或map多重绑定可以被声明多次。Dagger不会实现或调用@Multibinds.

Alternative: @ElementsIntoSet returning an empty set

对于空的set,作为替代方案,你可以在方法上添加@ElementsIntoSet.

@Module
class MyEmptySetModule {
  @Provides @ElementsIntoSet
  static Set primeEmptyFooSet() {
    return Collections.emptySet();
  }
}

Inherited subcomponent multibindings

subcomponent中的绑定可以依赖父component中的多重绑定set或map,就像其他绑定也是可以依赖父类的一样。subcomponent也可以向父component中绑定的多重绑定set或map添加元素,只需在module中添加合适的@Provides方法。

这种情况下,set和map根据注入的位置不同而不同。当它注入到定义在subcomponent的绑定时,它就会包含subcomponent和其父component的值或entry。当注入到父component中定义的绑定时,那它只包含此处定义的值或entry。

@Component(modules = ParentModule.class)
interface ParentComponent {
  Set strings();
  Map stringMap();
  ChildComponent childComponent();
}

@Module
class ParentModule {
  @Provides @IntoSet
  static String string1() {
    "parent string 1";
  }

  @Provides @IntoSet
  static String string2() {
    "parent string 2";
  }

  @Provides @IntoMap
  @StringKey("a")
  static String stringA() {
    "parent string A";
  }

  @Provides @IntoMap
  @StringKey("b")
  static String stringB() {
    "parent string B";
  }
}

@Subcomponent(modules = ChildModule.class)
interface ChildComponent {
  Set strings();
  Map stringMap();
}

@Module
class ChildModule {
  @Provides @IntoSet
  static String string3() {
    "child string 3";
  }

  @Provides @IntoSet
  static String string4() {
    "child string 4";
  }

  @Provides @IntoMap
  @StringKey("c")
  static String stringC() {
    "child string C";
  }

  @Provides @IntoMap
  @StringKey("d")
  static String stringD() {
    "child string D";
  }
}

@Test void testMultibindings() {
  ParentComponent parentComponent = DaggerParentComponent.create();
  assertThat(parentComponent.strings()).containsExactly(
      "parent string 1", "parent string 2");
  assertThat(parentComponent.stringMap().keySet()).containsExactly("a", "b");

  ChildComponent childComponent = parentComponent.childComponent();
  assertThat(childComponent.strings()).containsExactly(
      "parent string 1", "parent string 2", "child string 3", "child string 4");
  assertThat(childComponent.stringMap().keySet()).containsExactly(
      "a", "b", "c", "d");
}

Subcomponents

继承和扩展父component对象图的component称为subcomponent。你可以使用它们把你的应用划分为不同子图,封装为不同的模块或在component中使用不同地域。

subcomponent中绑定的对象可以依赖绑定在父级component中的任意对象和自己module中绑定的对象,但不能依赖兄弟级component中绑定的对象。

换句话说,subcomponent的父级component的对象图是这subcomponent对象图的子图。

Declaring a subcomponent

就像声明上层component一样,创建抽象类或接口并声明抽象方法返回你需要的类型,然后添加@Subcomponent注解而不是@Component,设置@Modules.

@Subcomponent(modules = RequestModule.class)
inferface RequestComponent {
  RequestHandler requestHandler();
}

Adding a subcomponent to a parent component

向父级component添加子component,只需在父级component添加返回值为子component的抽象工厂方法。如果子component需要一个没有无参构造器的module,需要在工厂方法添加该module类型的参数。这个工厂方法可能还有其他subcomponent中的module参数。(这个subcomponent会自动和parent component分享module实例)。

@Component(modules = {ServerModule.class, AuthModule.class})
interface ServerComponent {
  Server server();
  SessionComponent sessionComponent(SessionModule sessionModule);
}

@Subcomponent(modules = SessionModule.class)
interface SessionComponent {
  SessionInfo sessionInfo();
  RequestComponent requestComponent();
}

@Subcomponent(modules = {RequestModule.class, AuthModule.class})
interface RequestComponent {
  RequestHandler requestHandler();
}

SessionComponent中绑定的module可以依赖ServerComponent中绑定的module,RequestComponent中绑定module同时依赖SessionComponent和ServerComponent绑定的module.

你可以通过调用parent component的工厂方法来创建subcomponent的实例。

ServerComponent serverComponent = DaggerServerComponent.create();
SessionComponent sessionComponent =
    serverComponent.sessionComponent(new SessionModule(…));
RequestComponent requestComponent = sessionComponent.requestComponent();

通常你需要parent component中的对象绑定来创建subcomponent。为了完成这些,你可以基于任何component中的绑定都可以依赖这个component类型本身。

class BoundInServerComponent {
  @Inject ServerComponent serverComponent;

  void doSomethingWithSessionInfo() {
    SessionComponent sessionComponent =
        serverComponent.sessionComponent(new SessionModule(…));
    sessionComponent.sessionInfo().doSomething();
  }
}

Subcomponent builders

你也可以按照component builders的定义方式定为component定义builder.

@Component(modules = {ServerModule.class, AuthModule.class})
interface ServerComponent {
  Server server();
  SessionComponent.Builder sessionComponentBuilder();
}

@Subcomponent(modules = SessionModule.class)
interface SessionComponent {
  @Subcomponent.Builder
  interface Builder {
    Builder sessionModule(SessionModule sessionModule);
    SessionComponent build();
  }
}

ServerComponent serverComponent = DaggerServerComponent.create();
SessionComponent sessionComponent = serverComponent.sessionComponentBuilder()
    .sessionModule(new SessionModule(…))
    .build();

注入subcomponent builder
就像component本身一样,subcomponent builder也是绑定在对象图中的也可以被注入。所以与其注入component然后调用subcomponent builder方法不如直接注入builder。

 /** 注入subcomponent builder. 这比下面的方法要简单*/
  class SessionStarterInjectingSubcomponentBuilder {
    private final SessionComponent.Builder sessionComponentBuilder;

    @Inject SessionStarterInjectingSubcomponentBuilder(
        SessionComponent.Builder sessionComponentBuilder) {
      this.sessionComponentBuilder = sessionComponentBuilder;
    }

    Session startSession() {
      return sessionComponentBuilder
          .sessionModule(new SessionModule(…))
          .build()
          .session();
    }
  }

  /**
   * 注入component然后调用其工厂方法. 比上面的方法麻烦       */
  class SessionStarterInjectingComponent {
    private final ServerComponent serverComponent;

    @Inject SessionStarterInjectingComponent(ServerComponent serverComponent) {
      this.serverComponent = serverComponent;
    }

    Session startSession() {
      return serverComponent.sessionComponentBuilder()
          .sessionModule(new SessionModule(…))
          .build()
          .session();
    }
  }

注意:SessionStarterInjectingSubcomponentBuilder并不依赖ServerComponent。

Subcomponents and scope

将component划分为subcomponent的理由之一是使用scopes;在普通的没有域的绑定中,一个注入的类型可能每次拿到的是新的独立的实例。但如果这个绑定使用了域,在这个域的生命周期中所有的用户都能拿到同一个实例。

典型的域是@Singleton。使用singleton域注解绑定的用户都拿到同一个对象。

Dagger中,可以通过@Scope注解将component和域联系起来。这种情况下,component的实现持有所有绑定域的对象,所以它们就可以被复用。如果Module中的@Provides方法被一个域注解了,那么这个module只能设置给被同一个域注解的component。

@Inject构造器也可以被域注解注解。这些隐式绑定可被其他相同域的component或其后代component使用。被注解的实例将会绑定正确的作用域)。

subcomponent不可以与任何父级component
有相同的域,但两个互相独立的subcomponent可以绑定同一个作用域因为不会对哪里保存域对象造成歧义。(即使使用了相同的域注解,这两个subcomponent也拥有不同的域对象。)

例如:在下面的component树中,BadChildComponent拥有和其父亲RootComponent相同的@RootScpe,这是一个错误。但SiblingComponentOne和SiblingComponentTwo可以一起使用@ChildScope,因为不会对两个component中的同类型绑定造成混淆。

@RootScope @Component
interface RootComponent {
  BadChildComponent badChildComponent(); // ERROR!
  SiblingComponentOne siblingComponentOne();
  SiblingComponentTwo siblingComponentTwo();
}

@RootScope @Subcomponent
interface BadChildComponent {…}

@ChildScope @Subcomponent
interface SiblingComponentOne {…}

@ChildScope @Subcomponent
interface SiblingComponentTwo {…}

Subcomponents for encapsulation

使用subcomponent的另一个原因是将应用的不同部分封装。例如:如果你的服务器中有两个服务(或应用中的两个界面)共享一些绑定,如认证和授权的部分,但它们还有其他与对方没有关系的绑定。为每个服务或界面创建独立的subcomponent将共享的绑定放到parent component,这样就说得通了。在上面的例子中,FooRequestComponent和 BarRequestComponent是隔离的兄弟component。你可以把他们及其module结合到一个@RequestScope component中,但会产生冲突的绑定。

Details

Extending multibindings

像其他的绑定一样,parent component中的multibindings对其subcomponent也是可见的。但subcomponent也可以像父component绑定的map和set添加multibinding.其他的这类贡献只对该subcomponent和其子component的绑定可见,对其父component不可见。

@Component(modules = ParentModule.class)
interface Parent {
  Map map();
  Set set();

  Child child();
}

@Module
class ParentModule {
  @Provides @IntoMap
  @StringKey("one") static int one() {
    return 1;
  }

  @Provides @IntoMap
  @StringKey("two") static int two() {
    return 2;
  }

  @Provides @IntoSet
  static String a() {
    return "a"
  }

  @Provides @IntoSet
  static String b() {
    return "b"
  }
}

@Subcomponent(modules = Child.class)
interface Child {
  Map map();
  Set set();
}

@Module
class ChildModule {
  @Provides @IntoMap
  @StringKey("three") static int three() {
    return 3;
  }

  @Provides @IntoMap
  @StringKey("four") static int four() {
    return 4;
  }

  @Provides @IntoSet
  static String c() {
    return "c"
  }

  @Provides @IntoSet
  static String d() {
    return "d"
  }
}

Parent parent = DaggerParent.create();
Child child = parent.child();
assertThat(parent.map().keySet()).containsExactly("one", "two");
assertThat(child.map().keySet()).containsExactly("one", "two", "three", "four");
assertThat(parent.set()).containsExactly("a", "b");
assertThat(child.set()).containsExactly("a", "b", "c", "d");

Repeated modules

component和其任意一个subcomponent都设置了类型的module,那么所有这些component都会使用同一个该module实例。这意味着如果一个subcomponent工厂方法包含一个重复module作为参数或者你使用重复module调用subcomponent建造方法会造成错误。(后者在编译期无法检测,是一个运行时错误)。

@Component(modules = {RepeatedModule.class, …})
interface ComponentOne {
  ComponentTwo componentTwo(RepeatedModule repeatedModule); // COMPILE ERROR!
  ComponentThree.Builder componentThreeBuilder();
}

@Subcomponent(modules = {RepeatedModule.class, …})
interface ComponentTwo { … }

@Subcomponent(modules = {RepeatedModule.class, …})
interface ComponentThree {
  @Subcomponent.Builder
  interface Builder {
    Builder repeatedModule(RepeatedModule repeatedModule);
    ComponentThree build();
  }
}

DaggerComponentOne.create().componentThreeBuilder()
    .repeatedModule(new RepeatedModule()) // UnsupportedOperationException!
    .build();

Producers

Dagger Producers是一个使用Java实现异步依赖注入的Dagger扩展。

Overview

这里假设读者已经熟悉Dagger2API和Guava的ListenableFuture.

Dagger Producers提供了几种新的注解,@ProducerModule@Producers@ProductionComponent分别类比@Module,@Provides@Component.我们把@ProducerModule注解的类作为producer modules,@Produces注解的方法作为producer methods,@ProductionComponent注解的接口作为producer graphs(类比于modules,provider methods,和object graphs).

并发编程是一个难题,但是一个强大而简单的抽象可以显著的简化并发的编写。出于这样的考虑,Guava 定义了 ListenableFuture接口并继承了JDK concurrent包下的Future 接口。

所以我没有继续翻译这篇文档。详情点击ListenableFuture,详情点击Producers

Testing

使用依赖注入框架会使测试变得更简单。本文档中探索了一些测试使用了Dagger的应用的策略。

Don't use Dagger for unit testing

如果你想写一个小的单元测试测试一个@Inject注解的类,不需要在测试代码中使用Dagger,只需调用@Inject注解的构造器和方法并设置给@Inject注解的字段即可,也可以直接传入模拟的依赖对象。

final class ThingDoer {
  private final ThingGetter getter;
  private final ThingPutter putter;

  @Inject ThingDoer(ThingGetter getter, ThingPutter putter) {
    this.getter = getter;
    this.putter = putter;
  }

  String doTheThing(int howManyTimes) { /* … */ }
}

public class ThingDoerTest {
  @Test
  public void testDoTheThing() {
    ThingDoer doer = new ThingDoer(fakeGetter, fakePutter);
    assertEquals("done", doer.doTheThing(5));
  }
}

Replace bindings for functional/integration/end-to-end testing

功能测试/综合测试/端对端测试一般使用生产环境的应用,但使用fakes替换persistence,后端和验证系统,让其他部分正常运行。这种方式适用于一个或少量有限数量的测试配置替换产品配置中的一些绑定。

Option 1: Override bindings by subclassing modules (don’t do this!)

最简单的方法是通过子类重写module的@Provides方法来替换待测component中的绑定。(看下面会出现的问题).

当创建Dagger component的实例时,你传入需要的module实例。你可以这些module子类实例,这些子类可以重写module中的@Provides方法来替换一些绑定。

@Component(modules = {AuthModule.class, /* … */})
interface MyApplicationComponent { /* … */ }

@Module
class AuthModule {
  @Provides AuthManager authManager(AuthManagerImpl impl) {
    return impl;
  }
}

class FakeAuthModule extends AuthModule {
  @Override
  AuthManager authManager(AuthManagerImpl impl) {
    return new FakeAuthManager();
  }
}

MyApplicationComponent testingComponent = DaggerMyApplicationComponent.builder()
    .authModule(new FakeAuthModule())
    .build();

但这种方法也有一些局限性

  • 不能改变绑定图的静态图形:不能添加或移除绑定或改变绑定的依赖。具体讲:
    • 重写@Provides方法不能改变其参数类型,缩小返回类型的范围也不会对绑定图造成影响。上面的例子中,testingComponent扔需要为AuthManagerImpl绑定以及其他的依赖,即使它们没有被用到。
    • 同样地,重写的module不能添加绑定到对象图,包括multibinding(但你仍可以重写一个SET_VALUES方法来返回一个不同的set)。任何子类中新的@Provides方法都会被Dagger忽略。这意味着虚拟的对象几乎不能使用到依赖注入的优势。
  • 这种方式复写的@Provides方法不能是静态的,所以不能省略它们的实例。

Option 2: Separate component configurations

另一个方法需要对module进行更多的前期设计。应用的每个配置(生产和测试)使用不同的component配置。这个测试component类型继承了生产环境component并配置了不同的modules.

@Component(modules = {
  OAuthModule.class, // real auth
  FooServiceModule.class, // real backend
  OtherApplicationModule.class,
  /* … */ })
interface ProductionComponent {
  Server server();
}

@Component(modules = {
  FakeAuthModule.class, // fake auth
  FakeFooServiceModule.class, // fake backend
  OtherApplicationModule.class,
  /* … */})
interface TestComponent extends ProductionComponent {
  FakeAuthManager fakeAuthManager();
  FakeFooService fakeFooService();
}

现在测试调用的主方法是DaggerTestComponent.builder()而不是DaggerProductionComponent.builder().注意此test component接口可以添加虚拟实例(fakeAuthManager()和fakeFooService())句柄这样需要的时候就可以拿到它们控制线束。

但你会怎样设计你的modules让这个模式更简单呢?

Organize modules for testability

Module类是一种工具类:是包含很多@Provides方法的集合,每个@Provides方法都可以作为一个注入器提供指定类型实例。

(一个@Provides方法依赖另一个提供的类型会使几个@Provides方法产生联系,但通常它们不会明确地调用对方或依赖同一可变状态。多个@Provides方法指向同一实例字段,这样它们就不再是独立的了。这里的建议是将@Provides方法视为工具方法这样测试时更易替换module)。

那么怎么决定哪些@Provides方法应该放在一个module中呢?

一种方式是将bindings分为published bindings和internal bindings,然后再决定那些published bindings有合适的选择。

Published bindings(公有绑定) 是这些向应用的其他部分提供功能的绑定。如AuthManager 或 User 或 DocDatabase 都是 published:他们都绑定在一个module中这样应用其他部分就可以使用他们。

剩下的绑定就是Internal(私有绑定) bindings:这类绑定在一些published 类型的实现中作为一部分被使用。例如:OAuth client ID或OAuthKeyStore的配置绑定只会被OAuth的实现AuthManager使用,不会被应用的其他部分使用。这些绑定通常是package-private的或被package-private修饰。

一些published 绑定会有替代选择,特别是测试时,其他的就没有。例如:AuthManager就有可选绑定:一个测试用,其他适用于不同的授权/验证协议。

但另一方面,如果AuthManager接口有一个方法返回当前在线用户,你可能想发布一个绑定提供Users,仅通过调用AuthManager的getCurrentUser()即可。这个published绑定就不太可能需要替代了。

一旦你将绑定分为带有替代选择的published绑定、没有替代选择的published绑定和internal绑定,可以这样编排modules:

  • 每个带替代选择的published绑定对应一个module。(每一个替代选择也对应一个module。)这个module仅包含一个published绑定,以及所有这个published 绑定需要的internal 绑定。
  • 所有无替代选择的published bindings放入按照功能线组织的module中
  • 公有绑定module应该包含需要公有绑定的没有替代选择的module.

为每个module加上文档描述它提供的公有绑定自然是极好的。

这是使用auth domain的例子。如果有一个AuthManager接口,它可能有一个OAuth实现和一个测试用的模拟实现。综上所述,可能有一个你并不像改变配置的关于当前用户的绑定。

/**
 * Provides auth bindings that will not change in different auth configurations,
 * such as the current user.
 */
@Module
class AuthModule {
  @Provides static User currentUser(AuthManager authManager) {
    return authManager.currentUser();
  }
  // Other bindings that don’t differ among AuthManager implementations.
}

/** Provides a {@link AuthManager} that uses OAuth. */
@Module(includes = AuthModule.class) // Include no-alternative bindings.
class OAuthModule {
  @Provides static AuthManager authManager(OAuthManager authManager) {
    return authManager;
  }
  // Other bindings used only by OAuthManager.
}

/** Provides a fake {@link AuthManager} for testing. */
@Module(includes = AuthModule.class) // Include no-alternative bindings.
class FakeAuthModule {
  @Provides static AuthManager authManager(FakeAuthManager authManager) {
    return authManager;
  }
  // Other bindings used only by FakeAuthManager.
}

然后你的正式环境配置将会使用真正的module,和测试配置使用虚拟module,如所述。

Project Pages

GitHub
Release 2.0 API(javadoc)
Developer API(javadoc)

因为官方工程是基于maven构建的,为了便于各位Android Coder的学习,我将官方工程中Android的部分拿出来放到GitHub上了。

回到顶部