Dagger2 User's Guide

文章翻译自Dagger官网,翻译水平有限,见谅。

引入

引用官网的引入说明,上面的部分都好理解,就是简单的compile,然后说如果Studio是2.2的话,推荐去升级Studio。
如果你和Databinding一起用的话,那么需要看这个Issue 306
这样就可以使用Dagger。

使用Dagger

我们将通过一个coffee maker项目来演示依赖注入和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实例给heaters成员变量,并且获得一个Pump实例给pump成员变量。

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

    ...
}

如果你的类中有@Inject注解的成员变量,但是没有@Inject注解的构造函数,Dagger将要注入被请求的成员变量,但是不会创建新的实例。 通过添加一个使用@Inject注解的空参构造函数可以是得Dagger正常的创建实例。

Dagger 同样支持方法注入,尽管构造函数和成员变量类型的注入更受欢迎。

没有被@Inject注解的类是不能通过Dagger构造的。

满足依赖

默认情况下,Dagger能够通过以上的请求方式构建每一条依赖对象的实例。当你想要获取一个CoffeeMaker,Dagger将通过 new CaffeeMaker() 并且设置它的可注入的成员变量。

但是@Inject也有缺陷:

  • Interfaces 不能够构建

  • 第三方类不能够被注解

  • 可配置的类必须是配置的

例如,当一个Heaters实例被请求的时候会调用 provideHeater() 这个方法:

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

通过@Provides这个注解使得拥有他们自己的依赖成为可能(这是什么翻译,手动捂脸)。
这个是返回一个 Thermosiphon 当一个Pump 被请求的时候:

@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注解的方法使用provide做方法名的前缀,使用@Module注解的类使用module做类名的后缀。

建立依赖图

这些使用@Inject和@Provides注解的类通过它们的依赖连接在一起。调用项目的main方法或者是一个安卓程序通过一个明确定义的树状图。这Dagger2中这个集合(前面说的树状图)是通过一个拥有空参的方法的接口并且返回要求的类型。通过提供@Component注解给这样额接口并且提供moduley一个@module的参数。之后Dagger2能够生成一个具体的关系。

我再用白话简单说一下,主要意思就是为了使得被@Inject和@Provides注解的类能够相互依赖,可以使用@Component注解并且设置module的value,这样@Inject的注解就能和@Provides的注解相互依赖了。

这里其实应该可以注意到的就是@Inject注解的成员变量对应的构造函数必须也得是@Inject注解过的,但是@Inject有的情况下是无法使用的上文也说了,可以是用@Provides注解解决这个问题,但是这又出现一个新的问题,@Inject注解怎么和@Provides注解进行沟通?答案是使用@Component。

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

这个接口是实例拥有和接口相同的类名,不过有Dagger前缀。可以通过这个实例调用 builer() 方法来获得一个实例对象,设置对应module,然后通过 build()方法来返回一个实例对象。

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

注:如果你的@Component注解的类不是顶级类型的话,这个生成的Component的名字将包含所有的闭合类型的名字,通过下划线拼接,例如下面你的代码:

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

将会生成一个DaggerFoo_Bar_BazComponent 命名的Component。

任何一个可以获得默认构造函数的module会被忽略作为Builder 将会创建实例对象想,如果没有设置的话。并且所有的module中被@Provides注解的方法都是static的。这个实例根本不需要一个具体的对象。如果所有的实例可以创建通过用户生成一个依赖对象的话,那么构建实例也将会有一个create()方法用来获取一个新的实例对象,不用通过使用builder。

CoffeeShop coffeeShop = DaggerCoffeeShop.create();

现在我们的CofferApp能够简单的使用Dagger生成一个CoffeeShop的实例去获得一个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:

  • 这些包含被@Provides声明的方法 @Module注解的类可以直接被@Component.modules直接引用,也可以间接的通过@Module.includes引用。

  • 任何被@Inject注解的构造函数是不在作用域内的,或者拥有一个@Scope注解来匹配一个Component范围。

  • component 依赖的提供方法。

  • component 他自己

  • 包含subcomponent的不限定的builders

  • Provider或者Lazy包装的以上的绑定

  • Provider的Lazy的以上的绑定例如 Provider<Lazy<CoffeeMaker>>

  • 任何类型的MemberInjector.

单例和范围绑定

通过使用@Provides和@Singleton注解方法或者可以注入的类,这种依赖关系图将使用一个单例实例提供给所有的客户端。

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

这个@Singleton注解在一个可注入的类上也作为记录提供。它提醒着使用者这个类可能会在多线程使用。

@Singleton
class CoffeeMaker {
    ...
}

自从Dagger2连接范围内的实例通过componet的实例对象,这些Components他们自己需要声明他们感兴趣的范围。例如,在同一个Component中是不可能同时有一个@Singleton绑定和一个@RequestScoped绑定,因为这两个范围用于不同的生命周期并且因此必须放在声明周期不同的Components中,为了声明一个关联已经提供的scope的Component,简单的提供范围注解到这个Component接口中就可以。

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

Components可能会拥有多种范围注解。这些声明的别名都是相同的,所以Component可能包含他声明的任意一个范围类型。

复用范围

有时候你可能会限制使用@Inject注解的构造函数的实例化或者@Provides注解方法被调用的数量,但是你并不需要保证这些需要的实例是在特别的Component或者子Component整个生命周期被使用,在Android 环境中这会是有用的。或者其他的配置是昂贵的系统中。

为了这些绑定,你可以提供@Resuable 范围。 @Resuable范围绑定,不像是其他的范围绑定,是不会和其他的任何单独的Component关联的。反而 每一个compoent实际上能够用这些绑定通过返回的缓存或者是实例的具体类。

这就意味着,如果你创建一个module利用了@Resuable绑定在一个Component中,但是只有一个子Component实际上使用了这个这个绑定。然后只有这个子Component缓存了这个绑定的实例。如果两个子Component没有通过绑定共享同一个父Component,每一个都将会缓存他们自己的实例,如果一个component的父Component已经缓存了这个类,那么它的子Component将会使用这个对象。

这里没有任何的保证Component会只调用这种绑定一次,所以使用@Resuable去绑定并且返回易变的类,或者是涉及到相同实例的类是危险的。它是安全的对于不变的类就是如果你不关心他们被分配多少次。

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

可释放的引用

当一个绑定使用范围注解的时候,这就意味着这个Component持有一个实例对象的引用,直到这个Component实例被GC回收(这样就会有一个问题就是如果Component没有回收的话,那么它持有的所有引用也无法释放内存)。在内存敏感的环境中,例如Adnroid,你也许会想要让范围的类,那些在目前没有被正在使用类在GC回收的时候删掉,在项目在有内存压力的情况下。

在这种情况下,你可以定义一个范围并且使用@CanReleaseReferences注解。

@Documented
@Retention(RUNTIME)
@CanReleaseReferences
@Scope
public @interface MyScope {}

当你决定你将允许这个范围内的持有的类在GC期间是可以被删除的,如果它们没有被一些其他类正常的使用,你可以注入一个ReleasableReferenceManager类给你的范围,并且调用 releaseStrongReference() 方法,这将使得Component持有一个弱引用对象而不是强引用对象。

@Inject @ForReleasableReferences(MyScope.class)
ReleasableReferenceManager myScopeReferenceManager;

void lowMemory() {
  myScopeReferenceManager.releaseStrongReferences();
}

如果你决定内存压力消退,然后你可以恢复这些强引用给任何缓存的类-在GC期间还没有被缓存的实例对象通过调用restoreStrongReferences()

void highMemory() {
  myScopeReferenceManager.restoreStrongReferences();
}

懒注入

有时你可能会想延迟获取一个类的实例对象。对于任何的绑定T,你可以是用Lazy<T> 延迟实例化直到第一次调用Lazy<T>.get()方法,如果 T 是 Singleton ,那么Lazy(T)将是同一个实例在所有的注入的类图中。另外,每一个注入的位置将拥有它们自己热Lazy<T>实例。无论,后来的调用去获取一个Lazy<T> 的实例将会获取一个之前的相同的T的实例。

class GrindingCoffeeMaker {
  @Inject Lazy<Grinder> lazyGrinder;

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

提供者注入

有时你需要多种实例对象返回,而不是单个的实例。当你拥有几种方案的时候(例如工厂,建造者等),一个选择是注入一个Provider<T>而不仅仅是 T ,一个Provider<T>调用构建的逻辑在每次.get()方法调用的时候。如果绑定的逻辑是@Inject的构造函数,一个新的实例将被创建,但是一个@Provides注解的方法没有这样的保证。

class BigCoffeeMaker {
  @Inject Provider<Filter> 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<T>可能有令人困惑的代码,并且可能是一个缺少范围或者是缺少组织的设计在你的关系类图中,通常你会使用工厂或者是一个Lazy<T>或者是重组代码的机构和使用期来能够注入一个T 。然而注入的Provider<T>能够实现,在一些具体的案例中像一个拯救者。一个使用情景就是当你必须使用一个遗留的架构,这个架构不能够整合你的类的自然生命期。例如servlets是设计单例的,但是只有当请求特殊的数据的情境下是可用的。

限定符

有时类型本身是不足以确定依赖的。例如:一个高级的咖啡机是应该是能够区分水和加热的底座。

在这种情况下,我们需要一个限定的注解,这是任意的注解只要它本身被@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);
}

依赖可能不会拥有多种限定注解。

可选绑定

如果你想一个绑定能够使用尽管一些依赖没有在这个Component内的话,你可以添加一个@BindsOptionalOf注解的方法到一个Module:

@BindsOptionalOf abstract CoffeeCozy optionalCozy();

这就一位置被@Inject注解的构造函数和成员变量还有@Provides注解的方法可以依赖在一个Optional<CoffeeCozy> 类。如果在一个Componet中有这个的一个绑定的话,那么Optional将会被呈现,如果这里没有CoffeeCozy的绑定,那么Optional将会缺少。

特别的你可以注入下面你的任意一个:

  • Optional<CoffeeCozy>

  • Optional<Provider<CoffeeCozy>>

  • Optional<Lazy<CoffeeCozy>>

  • Optional<Provider<Lazy<CoffeeCozy>>>

(当然你也可以注入一个Provider或者Lazy或者Provider包含Lazy,但是这不是很有用)

在Component中一个可选的绑定是缺失的,但是能够在子的Component中被呈现,如果这个子Component包含包含一个绑定潜在的类型

你也以用 Guava’s Optional or Java 8’s Optional.

绑定实例

经常你需要有效的数据在构建Component时。例如 假设你有一个项目使用命令行参数;你需要绑定这些参数在你的Component中。

也许你的app需要一个单独的参数代表用户的名字,你或许会使用注解@UserName String 。你可以添加一个呗@BindsInstance 注解了的方法到这个Component builder 去允许这个实例对象被注入到这个Component中。

@Component(modules = AppModule.class)
interface AppComponent {
  App app();

  @Component.Builder
  interface Builder {
        @BindsInstance Builder userName(@UserName String userName);
        AppComponent build();
  }
}

你的app 看起来可能是这样的:

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 在这个Component中将会作为一个实例提供给Builder 当调用这个方法的时候。
在构建Component之前,所有@BindsInstance方法必须被调用,传输一个非空的数据。

如果@BindsInstance注解的方法参数被@Nullable注解,那么这个绑定会被认为是可空的以相同的方式@Provides方法也是可以的。注入的位置也必须用@Nullable 标记,并且Null 是一个允许的数据去被绑定,更多的 Builder的使用者可能会遗漏调用方法,并且Component将会认为这个实例为Null。

@BindsInstance 方法应该更偏向写一个@Moudle通过构造函数参数,并且立即提供它们的值。

编译时检查

Dagger的注解解释器是严格的并且将会出现编译错误当任何的绑定是无效的或者是无法完成的。例如 ,这Module将被创建在一个Component钟,这个Component缺少一个绑定的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.

解决这个问题通过添加一个@Provides注解的方法用来提供Excutor给任意的Modules在这个Component中。当@Inject、@Module 和@Provides注解是单独有效的时候,所有的检查在这个绑定间的关系在@Component中。Dagger1的依赖在Module层严格检查(这也许会有运行时的反射动作),但是Dagger2省略了这个检查(并且随着配置的在@Module中的参数)在一个更好的全局依赖图中。

编译时代码生成

Dagger的注解解析器也许生成这种 CoffeeMaker_Factory.java 的文件或者CoffeeMaker_MembersInjector.java。这些是Dagger实现的细节。你不必直接使用它们,尽管它们在调试的时候通过注解很好使用。这里只有一个你更应该使用的在你的代码中国的应该是使用Dagger前缀的Component对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容