Dagger2 Android应用:@Scope和@Subcomponent

这部分会介绍Dagger2中比较莫名的概念,同样也不涉及Android的具体代码。
Dagger2使用中的核心技巧包括@Subcomponent和@Scope,这两个注解对架构的层次关系有非常重要的作用。

Dagger2的作用域 @Scope

作用域是Dagger2中一个比较重要的概念,各种奇淫巧技,比如单例,都是通过它来实现的。
按照Google的官方定义,Scope应该作为作用域来理解,然而很多中文资料将它翻译为生命周期。
这对很多Android开发者来说就导致概念模糊,这生命周期跟Android的生命周期又是个什么关系?
所以@Scope是什么,它的作用又是什么呢。

什么是@Scope

@Scope 的官方定义 Subcomponent & Scope

One reason to break your application’s component up into Subcomponents is to use scopes. With normal, unscoped bindings, each user of an injected type may get a new, separate instance. But if the binding is scoped, then all users of that binding within the scope’s lifetime get the same instance of the bound type.

这个解释比较抽象,它说明一个概念就是,在不使用@Scope的情况下,可能每次注入的对象都会是一个新的不同的对象,而@Scope能限制被注入的对象,在同一个@Scope的生命周期(lifetime)中都只存在一个且仅有一个对象(这不就是单例么)。
是的其实在Dagger2中,单例对象的实现方式就是用@Scope,Dagger2给开发者提供了一个默认的已经定义好的单例注解,@Singleton。
@Singleton 的源码是这样的

@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton{}

看上面的代码可以基本明白,@Scope它提供了一种自定义注解的方法,它本身并不能直接使用,开发者结合自己的需求,用@Scope来定义所需要的注解。
像@Singleton用来做单例注解的,Dagger2已经替我们做好,只要直接使用就行。

@Subcomponent

在尝试解释@Scope 的时候,我发现它实际上很难单独拎出来理解。@Scope所描述的作用域,是需要跟Component的层级结合才能使用的。这时候就需要结合@Subcomponent 来一起理解这两个东西了。

像之前的例子代码里,Machine被划分为Component,Heater和Pumper是作为Module来提供给Component依赖的。然而一个Componenta(组件)同时可以作为Module存在,反过来一个Module也可以作为Component存在。

理解了这个概念后,还需要理清一个点是,什么时候需要拆分为不同的层级(Component),不同层级之间是又是什么关系呢?

Google官方的建议,除了在需要使用@Scope的情况下使用@Subcomponent,还有一种情况可以拆分层级

Another reason to use Subcomponents is to encapsulate different parts of your application from each other. For example, if two services in your server (or two screens in your application) share some bindings, say those used for authentication and authorization, but each have other bindings that really have nothing to do with each other, it might make sense to create separate Subcomponents for each service or screen, and to put the shared bindings into the parent component.

意思是说,如果不同组件间互相没有依赖或者关联,那么可以把他们的共同使用到的部分放到 parent component中,而这俩就可以作为Subcomponent存在。
这段话说明
· child component 可以使用 parent component 中的依赖
· parent component 可以有多个 child component,而且互相无关

使用 @Scope @Subcomponent 来拆分层级

在不使用@Subcomponent的情况下修改之前的代码

我们回到之前CoffeeMachine的例子。老板要加个咖啡师(CoffeeMaker),咖啡师操作Heater来加热,而且咖啡师是属于 CoffeeMachine 层级的,就是说它应该是Machine的成员,或者以依赖存在。
Heater的构造方法此时就需要加入Maker对象,且必须以入参方式存在,而不能在构造方法里直接new一个,原因相信思考一下就明白。
我们需要修改两处地方,一个是Heater的构造方法,一个是HeaterModule的provide方法。还记得provide的作用么,它是用来提供依赖的对象来注入到Component的,通过它,Heater的实例化从CoffeeMachine中剥离,单独存在于Module中。

public class ElectricHeater implements Heater {

    Cooker mCooker;

    private static final String TAG = PublicConstant.PUB + "ElectricHeater";

    public ElectricHeater(Cooker cooker) {
        this.mCooker = cooker;
    }

    @Override
    public void heat() {
        Log.d(TAG, "electric heating...");
    }
}

Module类则变成了这样,

@Module
public class HeaterModule {

    Cooker mCooker;

    public HeaterModule(Cooker cooker) {
        this.mCooker = cooker;
    }

    @Provides
    public Heater provideHeater() {
        return new ElectricHeater(this.mCooker);
    }
//    @Provides
//    public Heater provideHeater() {
//        return new ElectricHeater(new Cooker());
//    }
}

实际上Module类的provide方法也可以像注释掉的代码那部分这么写,一样也可以做到给ElectricHeater注入Cooker的目的,然而这样很明显是错误的,因为这样一来Machine就不能管理Cooker,还记得Cooker是属于Machine层级这个设定吗?

接下来需要修改Machine的代码了,它所需要的只是在build component的时候指定HeaterModule并传入Cooker实例,

public class CoffeeMachine {
    private static final String TAG = PublicConstant.PUB + "CoffeeMachine";

    @Inject Heater mHeater;
    @Inject Pumper mPumper;
    Cooker mCooker;

    public void makeCoffee() {
        mCooker = new Cooker();
        DaggerMachineComponent.builder()
                .build()
                .inject(this);
        mPumper.pump();
        mHeater.heat();
        Log.d(TAG, "Coffee ready");
    }
}

有些人就会疑惑,万一忘了修改Machine的注入代码,没有传个HeaterModule会怎样呢?
Dagger2会在运行期判断这种情况,如果是一个不需要参数的Module,那么它在没有传入module实例的时候没有任何问题,Dagger2帮你实例化一个module对象;对于需要参数的module而我们又忘了设,那么它会丢个IllegalStateException出来。

使用@Subomponent 和 @Scope 拆分层级

虽然在不用@Subcomponent的情况下也可以实现从Machine里将Cooker传给Heater,但这种方式代码的层级不够明确。于是我们用这两个注解来进一步拆分。
现在可以看出来Heater也需要依赖Cooker了,我们将Machine和Heater的关系重新审视一下,看成是 parent component 和 child component 的关系。实际上这种例子在Andorid开发中常常是以 Application 和 Activity 的形式存在,我们经常遇到的会像在Activity中需要用到全局的SharePreference,或者LogUtils工具,或者像图片框架/线程池/网络请求框架等情况,这时候我们会把这些东西放到Application中去。

定义Scope

在这个例子里我们至少需要定义两个Scope,为了整体代码的统一,我们定义三个Scope,分别是Machine/Heater/Pumper层级。需要注意的是,Machine是相对于另外两个更高的层级,可以理解为Parent Scope,它的范围更大,另外的Heater和Pumper是同一个层级。这里我们是这么假设的。

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerMachine {
}

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerHeater {
}

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerPumper {
}

定义@Scope之后,有对应@Scope的module就只能在注解了同样@Scope中的Component里使用了,而且Subcomponent不能使用Parent Component同样的@Scope,这样代码的层级就明确的划分了开来。

定义Subcomponent

首先我们把Heater独立为一个Component,这里面会有一个问题,
Component在注入的时候不能用父类或者接口作为参数,在我们这个例子里,必须以ElectricHeater 作为参数,而不能用Heater。

@PerHeater
@Subcomponent
public interface HeaterSubcomponent {

    void inject(ElectricHeater heater);
}

我们用@Subcomponent来表示这是个子组件,它所在的层级由@PerHeater来表示,而它所需要注入的组件是 EletricHeater
再次注意inject()方法的参数不能用接口也不能用父类

在定义了Subcomponent之后,它是怎么跟Parent Compoenent联系起来的呢?这里扯个题外话,在Dagger1的时候,并没有@Subcomponent注解,它是用depenencies来表明Subcomponent的。在Dagger2中,我们只需要在Parent Component中显式的定义一个Subcomponent的接口即可。

@PerMachine
@Component(modules = { HeaterModule.class,
        PumperModule.class,
        CookerModule.class})
public interface MachineComponent {

    HeaterSubcomponent heaterComponent();//表明Subcomponent

    void inject(CoffeeMachine machine);
}

@PerMachine 是用来表示它的层级的,如果Subcomponent注解了同样的层级,那么编译时就会报错。CookerModule是新增的module,用来提供Cooker给Machine,之后包括Machine和Heater都可以使用这个实例,可以把Cooker认为是一个"全局"实例。

@Module
public class CookerModule {

    @PerMachine
    @Provides
    public Cooker provideCooker() {
        return new Cooker();
    }

}

这里可以看到@PerMachine也用来注解provide方法,这样一来就表示Cooker会存在于Machine层级中,从代码角度来说,Cooker() 实例会在MachineComponent中存在,而且只要是通过MachineComponent去获取的,那么就只有一个。
注意,在Dagger2中的单例,应该理解为在它所注解的@Scope的Component中只存在一个,这与通过private constructor + getInstance()来实现的单例有所不同

然后我们回到CoffeeMachine的代码,我们在这里通过注入增加一个Cooker的对象

public class CoffeeMachine {
    private static final String TAG = PublicConstant.PUB + "CoffeeMachine";

    @Inject
    Pumper mPumper;
    @Inject
    Cooker mCooker;
    @Inject
    Heater mHeater;

    public CoffeeMachine() {
    }

    static MachineComponent component;

    public void makeCoffee() {
        component = DaggerMachineComponent.create();
        component.inject(this);
        mPumper.pump();
        mHeater.heat();
        Log.d(TAG, "Coffee ready, cooked by: " + mCooker);
    }

    //通过静态方法向子类暴露Component
    public static MachineComponent getComponent() {
        return component;
    }
}

需要注意这个注释的地方,子类需要通过父类的静态方法来获取Parent Component,进而对自己进行注入。然后来看ElectricHeater的代码,这时候我们需要直接注入Cooker实例到ElectricHeater中

public class ElectricHeater implements Heater {
    private static final String TAG = PublicConstant.PUB + "ElectricHeater";

    @Inject
    Cooker mCooker;

    public ElectricHeater() {
      //DaggerMachineComponent.create().heaterComponent().inject(this);
      //注意这里我们用Parent的静态方法获取Component
      CoffeeMachine.getComponent().heaterComponent().inject(this);
    }

    @Override
    public void heat() {
        mCooker.cook();
        Log.d(TAG, "electric heating... by: " + mCooker);
    }
}

现在我们在ElectricHeater中也获取了Cooker,而且是从Machine拿到的Cooker。为了验证我们通过log来看是否是同一个对象,


Dagger的单例

聪明的你应该已经明白这是怎么回事了,这也是Dagger2中单例的实现方式。

有人可能有疑问,为什么必须用Parent的静态方法来获取Component进行注解呢?其实静态方法不是必须的,只要能拿到Parent的同一个Component就可以。
那么是否可以直接用DaggerMachineComponent来注入呢?这是个有意思的问题,我们修改代码来尝试下。

public ElectricHeater() {
  DaggerMachineComponent.create().heaterComponent().inject(this);
  //CoffeeMachine.getComponent().heaterComponent().inject(this);
}

我们注释掉用静态方法获取Component的方式,通过DaggerMachineComponent来获取Subcomponent进行注入。最后我们会看到log如下


重复实例化的结果

所以明白为什么需要获取Parent Component的同一个Component了吧。因为Cooker是保存在Component中的,如果我们重新用DaggerMachineComponent来进行注入,那么就会产生新的Component,同样Cooker也会是新的。

很多资料用@Singleton来说明单例,其实像之前说的,@Singleton也是自定义注解,只是它是Dagger2提前给我封装好可以直接用而已。对于开发来说,想让某个对象成为单例,同样要用@Singleton来同时注解Module的Provide方法和Component。而这个跟我们用@PerMachine来注解Cooker和Machine是一个道理。

总结

到现在应该可以理解什么时候需要用@Scope和@Subomponent了。
当需要划分层级的时候,我们会把某个组件作为单独的Component划分出去,这时候它通过@Scope定义为比Parent Component范围小的作用域,它可以在不声明依赖的module的情况下使用Parent Component中的依赖。
但是有个限制,Parent Component的依赖只能往下暴露一层,这意味着如果有多层结构的话,每个Subcomponent都需要手动声明一个向下暴露依赖的接口。
同时Component中的inject方法不能是接口也不能是父类,因此这会有个限制,拿MVP来做例子,通常只能是其中的M/V/P以层的方式来作为Component。这个概念需要在实践中才能体会。。

所有这个例子中的代码已经可以从这里下载: MyDagger2

你可能感兴趣的:(Dagger2 Android应用:@Scope和@Subcomponent)