Dagger2-User's Guide

V2.16


Summary

在任何应用程序中,最好的类都是那些做事情的类:BarcodeDecoder、KoopaPhysicsEngine和AudioStreamer。这些类有依赖性;可能是BarcodeCameraFinder、DefaultPhysicsEngine和HttpStreamer。

相比之下,任何应用程序中最糟糕的类都是那些占用空间却不做太多事情的类:BarcodeDecoderFactory、CameraServiceLoader和MutableContextWrapper。这些类就像笨拙的管道胶带,将有趣的东西连接在一起。

Dagger是对这些FactoryFactory类的替换,它实现了依赖注入设计模式,无需编写模板文件。它可以让你专注于有趣的类。声明依赖项,指定如何满足它们,并发布应用程序。

通过构建标准的javax-inject注释(JSR 330),每个类都易于测试。你不需要大量的模板就可以将RpcCreditCardService转换为FakeCreditCardService。

依赖注入不仅仅用于测试。 它还可以轻松创建可重用、可互换的模块。 您可以在所有应用程序中共享相同的AuthenticationModule。 您可以在开发期间运行DevLoggingModule,在生产中运行ProdLoggingModule,以获得每种情况下的正确行为。

Why Dagger 2 is Different

依赖注入框架已经存在多年了,它使用各种各样的api进行配置和注入。那么,为什么要重新发明轮子呢?Dagger 2是第一个使用生成的代码实现完整堆栈的工具。指导原则是生成代码,该代码模仿用户可能手写的代码,以确保依赖注入尽可能简单、可跟踪和高性能。要了解更多的设计背景,请观看+Gregory Kick的演讲(幻灯片)。

Using Dagger

我们将通过构建一个咖啡机来演示依赖注入和Dagger。有关可以编译和运行的完整示例代码,请参见Dagger的咖啡示例。

Declaring Dependencies

Dagger构造应用程序类的实例并满足它们的依赖关系。 它使用javax.inject.Inject注释来标识它感兴趣的构造函数和字段。

使用@Inject来注释Dagger应该用来创建类实例的构造函数。 当一个新的实例被请求时,Dagger将获得所需的参数值并调用这个构造函数。

class Thermosiphon implements Pump { private final Heater heater; @Inject Thermosiphon(Heater heater) { this.heater = heater; } ... }

Dagger可以直接注入字段。在本例中,它获得了Heater的实例heater和Pump的实例pump。

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

如果您的类具有@ Inject注释的字段,但没有@注入注释的构造函数,Dagger会根据请求注入这些字段,但不会创建新的实例。 使用@Inject注释添加一个无参数构造函数,以指示Dagger也可以创建实例。

Dagger也支持方法注入,虽然通常建议使用构造函数或字段注入。

缺少@Inject注释的类不能由Dagger构造。

Satisfying Dependencies

在默认情况下,Dagger通过构造上面描述的请求类型的实例来满足每个依赖项。当您请求使用CoffeeMaker时,它将通过调用new CoffeeMaker()并设置其可注射字段来获得。

但是@Inject不是在任何地方都能用的:

    接口不能建造。

    第三方类不能被注释。

    必须配置可配置的对象!

对于那些@Inject不够或不方便的情况,可以使用@provides注释方法来满足依赖关系。方法的返回类型定义了它满足的依赖关系。

例如,当需要Heater时,就调用provideHeater() :

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

@Provides方法也可以有它们自己的依赖关系。每当需要Pump时,这个系统就会返回一个Thermosiphon:

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

所有@Provides方法都必须属于一个module(模块)。 这些只是具有@Module注释的类。

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

按照惯例,@Provides方法以提供前缀命名,模块类以Module后缀命名。

Building the Graph

@Inject和@ provides注释类由对象的依赖项链接而成。像应用程序的main方法或Android应用程序那样调用代码,通过一组定义良好的根访问该图形。在Dagger 2中,该集合由一个接口定义,该接口没有参数并返回所需类型。通过将@Component注释应用于这样的接口并将模块类型传递给modules参数,Dagger 2就可以完成生成该契约的实现。

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

该实现的名称与以Dagger为前缀的接口名称相同。 通过在该实现上调用builder()方法获取实例,并使用返回的构造器来设置依赖项并build()一个新实例。

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

Note: 如果您的@Component不是顶层类型,则生成的组件的名称将包含其封闭类型的名称,并加上下划线。 例如,这段代码:

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

将生成一个名为DaggerFoo_Bar_BazComponent的组件。

任何具有可访问默认构造函数的模块都可以省略,因为如果没有设置,构建器将自动构造一个实例。 对于任何其@Provides方法都是静态的模块,实现根本不需要实例。 如果所有的依赖都可以在没有用户创建依赖实例的情况下构建,那么生成的实现也会有一个create()方法,该方法可用来获取新实例而无需处理构建器。

CoffeeShop coffeeShop = DaggerCoffeeShop.create();

现在,我们的CoffeeApp可以简单地使用dagger生成的CoffeeShop实例来获得一个完全注入的CoffeeMaker。

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

现在已经构建好了图形,并注入了入口点,我们可以运行我们的coffee maker应用程序了。

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

Bindings in the graph

上面的例子展示了如何使用一些更典型的绑定来构建一个组件,还有多种机制可以为图形提供绑定。 以下是依赖关系,可用于生成格式良好的组件:

    那些由@Provides声明在@Module中的方法由@Component.modules直接引用,或者通过@ Module.includes传递。

    任何带有@Inject构造函数的类型,该类型是unscoped的,或具有与组件范围之一相匹配的Scope注释。

    为组件依赖关系的组件提供方法。

    组件本身。

    任何包含子组件(subcomponent)的均为不合格的建设者。

    Provider或Lazy包装器用于上述任何绑定。

    任何上述绑定的Lazy提供者(例如,Provider<Lazy<CoffeeMaker>>)。

    任何类型的MembersInjector。

Singletons and Scoped Bindings

使用@Singleton注释的@Provides方法或注入类。 该图形(graph)将为其所有客户端使用该值的单个实例。

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

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

@Singleton class CoffeeMaker { ... }

由于Dagger 2将图(graph)中的作用域实例(scope)与组件实现的实例关联,因此组件本身需要声明它们打算表示的作用域。例如,在同一个组件中有一个@Singleton绑定和一个@RequestScoped绑定没有任何意义,因为这些作用域有不同的生命周期,因此必须存在于具有不同生命周期的组件中。要声明组件与给定范围相关联,只需将范围注释应用到组件接口。

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

组件可能会应用多个范围注释。 这声明它们都是相同范围的别名,并且组件可以包含它声明的任何范围的范围绑定。

Reusable scope

有时,您希望限制实例化@ Inject类或调用@ provider方法的次数,但不需要保证在任何特定组件或子组件的生命周期中使用相同的实例。这在Android这样的环境中非常有用,因为在这些环境中,分配可能会很昂贵。

对于这些绑定,您可以应用@Reusable范围。 与其他范围不同,@Reusable范围的绑定不与任何单个组件关联;相反,实际使用绑定的每个组件都会缓存返回的或实例化的对象。

这意味着,如果您在组件中安装了一个具有@Reusable绑定的模块,但实际上只有子组件使用绑定,那么只有子组件将缓存绑定的对象。如果没有共享祖先的两个子组件各自使用绑定,那么每个子组件都将缓存自己的对象。如果组件的祖先已经缓存了对象,子组件将重用它。

不能保证组件只调用绑定一次,因此将@Reusable应用于返回可变对象的绑定,或者是引用相同实例的对象,这是很危险的。如果您不关心分配了多少次不可变对象,那么可以安全地为它们使用@Reusable。

@Reusable // It doesn't matter how many scoopers we use, but don't waste them. class CoffeeScooper { @Inject CoffeeScooper() {} } @Module class CashRegisterModule { @Provides @Reusable // DON'T DO THIS! You do care which register you put your cash in. // Use a specific scope instead. static CashRegister badIdeaCashRegister() { return new CashRegister(); } } @Reusable // DON'T DO THIS! You really do want a new filter each time, so this // should be unscoped. class CoffeeFilter { @Inject CoffeeFilter() {} }

Lazy injections

有时你需要一个对象懒惰地实例化。 对于任何绑定T,您可以创建一个Lazy,它延迟实例化,直到首次调用Lazy的get()方法。 如果T是一个单例,那么Lazy将成为ObjectGraph内所有注入的相同实例。 否则,每个注入站点将获得它自己的Lazy实例。 无论如何,对Lazy的任何给定实例的后续调用将返回相同的T的底层实例。

class GrindingCoffeeMaker { @Inject Lazy lazyGrinder; public void brew() { while (needsGrinding()) { // Grinder created once on first call to .get() and cached. lazyGrinder.get().grind(); } } }

Provider injections

有时你需要返回多个实例,而不是只注入一个值。 虽然有几个选项(工厂,构建器等),但一种选择是注入一个Provider<T>而不仅仅是T。一个Provider<T>每次调用.get()时调用T的绑定逻辑。 如果该绑定逻辑是@Inject构造函数,则会创建一个新实例,但@Provides方法没有这种保证。

class BigCoffeeMaker { @Inject Provider filterProvider; public void brew(int numberOfPots) { ... for (int p = 0; p < numberOfPots; p++) { maker.addFilter(filterProvider.get()); //new filter every time. maker.addCoffee(...); maker.percolate(); ... } } }

注意:注入Provider可能会产生混淆的代码,并且可能是图形中存在错误或错误结构对象的设计气味。 通常你会想要使用工厂或Lazy或者重新组织代码的生命周期和结构,以便能够注入T.注入提供者可以在某些情况下成为救命。 一个常见的用法是,您必须使用与对象的自然生存期不一致的遗留体系结构(例如,servlet按照设计是单件的,但仅在请求特定数据的上下文中有效)。

Qualifiers

有时仅仅这种类型不足以识别依赖性。 例如,一个复杂的咖啡机应用程序可能需要为水和热板分开加热器。

在这种情况下,我们添加一个限定符注释。 这是任何注释本身有一个@Qualifier注释。 以下是@Named的声明,它是javax.inject中包含的限定符注释:

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

您可以创建自己的限定符注释,或者仅使用@Named。 通过注释感兴趣的字段或参数来应用限定符。 类型和限定符注释都将用于标识依赖关系。

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

依赖项可能没有多个限定符注释。

Optional bindings

如果你想让绑定工作,即使组件中没有绑定某个依赖关系,也可以在模块中添加一个@BindsOptionalOf方法:

@BindsOptionalOf abstract CoffeeCozy optionalCozy();

这意味着@Inject构造函数和成员和@Provides方法可以依赖于一个可选的对象。 如果组件中存在CoffeeCozy的绑定,则可选将存在; 如果CoffeeCozy没有绑定,则可选将不存在。

具体而言,您可以注入以下任何一项:

        Optional (unless there is a @Nullable binding for CoffeeCozy; see below)

        Optional>

        Optional>

        Optional>>

(你也可以注入一个提供者或者懒惰或者懒惰的提供者,但这不是很有用。)

如果CoffeeCozy有一个绑定,并且该绑定是@Nullable,那么注入可选是一个编译时错误,因为Optional不能包含null。 您可以随时注入其他表单,因为Provider和Lazy总是可以从get()方法返回null。

如果子组件包含对基础类型的绑定,则可以在子组件中存在一个组件中不存在的可选绑定。

您可以使用Guava的Optional或Java 8的Optional。

Binding Instances

在构建组件时,您经常可以获得可用的数据。 例如,假设您有一个使用命令行参数的应用程序; 你可能想要在你的组件中绑定这些参数。

也许你的应用程序只需要一个参数来表示你想作为@UserName String注入的用户名。 您可以将一个注释@BindsInstance的方法添加到组件构建器,以允许将该实例注入组件。

@Component(modules = AppModule.class) interface AppComponent { App app(); @Component.Builder interface Builder { @BindsInstance Builder userName(@UserName String userName); AppComponent build(); } }

你的应用程序可能会是这样:

public static void main(String[] args) { if (args.length > 1) { exit(1); } App app = DaggerAppComponent .builder() .userName(args[0]) .build() .app(); app.run(); }

在上面的例子中,在组件中注入@UserName String将在调用此方法时使用提供给Builder的实例。 在构建组件之前,必须调用所有@BindsInstance方法,并传递一个非空值(除了下面的@Nullable绑定外)。

如果@BindsInstance方法的参数被标记为@Nullable,那么绑定将被认为是“可空的”,就像@Provides方法是可空的那样:注入站点也必须将其标记为@Nullable,并且null是可接受的值 绑定。 而且,Builder的用户可能会省略调用该方法,并且该组件会将该实例视为null。

@BindsInstance方法应该优先于用构造函数参数编写@Module并立即提供这些值。

Compile-time Validation

Dagger注释处理器是严格的,如果任何绑定无效或不完整,将导致编译器错误。 例如,该模块安装在缺少Executor绑定的组件中:

@Module class DripCoffeeModule { @Provides static Heater provideHeater(Executor executor) { return new CpuHeater(executor); } }

编译时,javac会拒绝缺少的绑定:

[ERROR] COMPILATION ERROR : [ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.

通过为Executor添加一个@ Provide-annotated方法来修复组件中的任何模块。 虽然@Inject,@Module和@Provides注释是单独验证的,但绑定之间的关系的所有验证发生在@Component级别。 Dagger 1严格依赖于@模块级验证(可能会或可能不会反映运行时行为),但Dagger 2将此类验证(以及@Module上随附的配置参数)取消为全图验证。

Compile-time Code Generation

Dagger的注释处理器也可以生成带有CoffeeMaker_Factory.java或CoffeeMaker_MembersInjector.java等名称的源文件。 这些文件是Dagger实现细节。 您不需要直接使用它们,但通过注入进行分步调试时,它们可能非常方便。 您应该在您的代码中引用的唯一生成的类型是为您的组件添加了Dagger的前缀。



后记:

Dagger的GitHub,翻译原文传送门

一篇非常不错的dagger2入门文章:Dagger2从入门到放弃再到恍然大悟

dagger2的中文资料没有太多,dagger-android的资料更小稀少,本来还想翻译完后再翻译一下Dagger & Android的,这会可能要重新考虑了。

谷歌官方的Android架构Demo,我是为了看懂里面的MVP+Dagger2架构,才走上学习和翻译官网的这条路,谷歌里面的架构的确值得我们学习。

翻译🉐️并不好,甚至可能晦涩难懂,对dagger2 研究和应用会继续进行。欢迎各路大神不吝赐教~!

推荐阅读更多精彩内容