阅读 410

出来混迟早要还的,技术债Dagger2(终章):@SubComponent解决“混乱”依赖关系

前言

实话实说,这是一个毁誉参半的框架。较陡的学习曲线和其所带来的收益并不成正比。而且一旦没办法carry全场的话,很容易一顿操作猛如虎,一看战绩0-5...

所以我决定在这篇文章结束后,暂时停止后续Dagger的文章。

一个框架的出现,一定是为了解决某一类问题。这就如同@Component@SubComponent关系一样。当我们使用@Component能够满足我们的日常开发时,@SubComponent是不会出现的,除非谁“脑子有泡”,瞎造轮子。

所以@SubComponent势必是为了解决@Component所不能解决的问题...

正文

本文参考了:Dagger 2 : Component Relationships & Custom Scopes

enter image description here

需要自备梯子

前系列文章,我们由渐入深的逐步应用了Dagger2:

出来混迟早要还的,技术债Dagger2:基础篇

出来混迟早要还的,技术债Dagger2:Android篇(上)

出来混迟早要还的,技术债Dagger2:Android篇(中)@Scope、@Singleton

出来混迟早要还的,技术债Dagger2:Android篇(下)进一步理解Dagger

一、隐隐出现的问题

项目之初,万物起源,业务小需求少,太极生两仪,两仪生四象,四象生八卦...亦步亦趋,代码结构简单,写起来爽,维护起来也不费劲。

此时我们使用Dagger时,通常从单个AppComponent和带有@Singleton的单个AppModule开始。

在这种情况下,Dagger生成一个DaggerAppComponent,该组件与AppModule具有HAS-A关系。完全ojbk,写起来也很爽,直来直去!

但是随着我们的App规模的增长,我们很快就会意识到AppModule开始变成一个拥有各种依赖的上帝模块(god module)。越来越多的需求出现,越来越多的依赖需要加入到AppModule,有用的没用的通通需要加入...

重构迫在眉睫...

这里我们会很顺其自然的想到一些方案:既然我们的AppModule开始变成一个拥有各种依赖的上帝模块。那我们拆Module不就ok啦。

一般来说拆Module,可以分为横向拆和纵向拆。

1.1、横向拆Module

我们大概会这样重构:

@Component(modules = {AppModule.class, ApiModule.class})
public interface AppComponent {
    // 省略
}
复制代码

1.2、纵向拆Module

@Component(modules = AppModule.class)
public interface AppComponent{
    // 省略
}

@Module(includes = ApiModule.class)
public class AppModule{
    // 省略
}
复制代码

这样似乎,“完美的解决了我们的问题”?但是事实是这样么?问题总是各种各样:

如果我们需要对不同的Module在不同@Scope,提供不一样生命周期的单例时。我们只能干瞪眼!因为无论再怎么拆Module,Component都是唯一了,也就代表了ComponentScope是唯一的。

因此我们需要进一步拆,那就是拆Component。

二、拆Component通用方案

方案一:低层Component依赖高层Component

此方案的思路和我们正常“拆Module”的思路很类似。也就是从高层Component中抽出抽象内容,然后下沉到低层Component之中。

这种方案的具体实现会在下文中具体展开。

方案二:@SubComponent

通过@SubComponent让我们不同的Component拥有类似于父/子类的继承关系。

这种方案的具体实现会在下文中具体展开。

接下来咱们会通给一个真正的例子,来感受一下这俩种不同方案下的拆Component的。

2.1、“需求评审”

假设我们现在需要一个用户模块,负责用户登录/登出等操作。

红框的内容不用纠结,其实就是一个“用户的名字”。

简单解释一下这个图的流程,从左到右,表示应用启动。然后通过LoginComponent“pikachu”(皮卡丘...看眼这篇文章的老外作者是个《神奇宝贝》的粉丝)这个用户登录,然后进去HomeComponent,也就是正常的操作页面,当然后边的ItemComponent也是(只不过是另一个模块的界面而已)。然后再次进入LoginComponent登出了账号...接下来“jiigglypuff”登录账号,重复上述过程...

2.2、代码依赖图

  • 1、AppComponent(@Singleton) :Singleton作用域组件,应用程序的主要组件。通常在Application上被创建并持久化。

  • 2、LoginComponent(@ActivityScope) :AppComponentSubcomponent。包含了只有LoginActivity才需要依赖的内容。

  • 3、UserComponent(@UserScope):依赖于AppComponent(但并不是SubComponent)。包含用户模块相关的依赖。该组件还包含另外两个Subcomponents(@ActvityScope) :HomeComponent和ItemsComponent。

  • 4、HomeComponent(@ActivityScope) :UserComponentSubcomponent。它还包含三个Fragment:ProfileFragment、StatsFragment和MovesFragment。

  • 5、ItemsComponent(@ActivityScope):UserComponentSubcomponent

基于上述文字,咱们来看一张图:

2.3、代码实现方案一:低层Component依赖高层Component

2.3.1、AppComponent

@Singleton
@Component(modules = arrayOf(AppModule::class))
interface AppComponent {
    // 用于@SubComponent,此时先按下不表    
    fun loginBuilder(): LoginComponent.Builder

    // 不需要关心BaseSchedulerProvider是干啥的。
    fun schedulerProvider(): BaseSchedulerProvider
}
复制代码

AppComponent作为我们的顶级Component,势必会被其他的Component所依赖。但是其他的Component无法直接持有AppComponent的实例,依次也就没办法通过AppComponent的实例,调用到它其中所提供的依赖。

这里可不是继承,所以没有办法像子类那样调用到父类的变量/方法。

这很好理解吧?就如同我们Android项目,module之间的implementation低层模块想要用高层模块,我们常用的方式就是在底层模块写一个接口(Service),然后高层模块去实现这个接口。然后将实现接口的具体类,以多态的形式register到底层模块中。这样就完成了底层模块调用高层模块

对于我们的Dagger也是如此。如果下层的Component想要使用我们的AppComponent同样也是需要在AppComponent中提供这样的接口方法。也就是fun schedulerProvider(): BaseSchedulerProvider

一会我们会在下级Component(UserComponent)中,来感受这个方法的作用。

2.3.2、MyApplication

顶级Component,AppComponent的实例化。

class MyApplication : Application() {
    lateinit var appComponent: AppComponent
        private set
        
    override fun onCreate() {
        super.onCreate()
        appComponent = DaggerAppComponent
		.builder()
		.application(this)
		.build()
    }
    
    companion object {
        lateinit var app: MyApplication
            internal set
    }
}
复制代码

2.3.3、UserComponent

@Component(dependencies = arrayOf(AppComponent::class), modules = arrayOf(UserModule::class))
@UserScope
interface UserComponent {
    @Component.Builder
    interface Builder {
        fun build(): UserComponent;
        
        @BindsInstance
        fun pokeMon(pokemon: Pokemon): Builder
    }
}
复制代码

Pokemon是一个关于用户信息的实体类,此Class包含了我们用户登录的返回信息。(不需要多么在意,脑海中YY下就OK了。)

简单解释一下:

  • 1、dependencies = arrayOf(AppComponent::class):告诉Dagger2UserComponent,依赖于AppComponent

  • 2、我们在UserComponent中声明@Component.Builder。对于UserComponent,它的实例化,只需要一个Pokemon实例,所以只需要一个@BindsInstance即可。

关于Component.Builder/@BindsInstance的用法咱们之前的文章提到过。

接下来近一步来看一下代码:

2.3.4、UserManager

UserComponent的实例化。

@Singleton
class UserManager @Inject constructor(private val service: PokemonService) {
    var userComponent: UserComponent? = null
        private set

    public fun createUserSession(pokemon: Pokemon) {
        userComponent = DaggerUserComponent.builder()
            .appComponent(MyApplication.app.appComponent)
            .pokeMon(pokemon)
            .build()
    }
}
复制代码

填一下上文中提到的坑:fun schedulerProvider(): BaseSchedulerProvider:

Dagger中如何通过AppComponent中的fun schedulerProvider(): BaseSchedulerProvider方法,为我们的下层Component生成提供依赖的代码呢?

直接上编译生成类DaggerUserComponent

public final class DaggerUserComponent implements UserComponent {
    private Provider<BaseSchedulerProvider> schedulerProvider;

    private void initialize(final Builder builder) {
	    this.schedulerProvider =
    	    		new AppComponent_schedulerProvider(builder.appComponent);
	    // 省略部分代码
    }

    private static class AppComponent_schedulerProvider 
					implements Provider<BaseSchedulerProvider> {
        private final AppComponent appComponent;
	
        @Override
	    public BaseSchedulerProvider get() {
 		    return appComponent.schedulerProvider();
	    }
	    // 省略部分代码
    }
}
复制代码

我们可以看到,AppComponent中的fun schedulerProvider(): BaseSchedulerProvider,在DaggerUserComponent中以private Provider<BaseSchedulerProvider> schedulerProvider;的形式存在。

Provider<BaseSchedulerProvider>是一个接口,会以内部类的形式被实现,而其中的get方法,是通过appComponent.schedulerProvider()对外提供的。

appComponent是在initialize(final Builder builder)中伴随着Builder一同传入。也就是我们实例化DaggerUserComponent时:appComponent(MyApplication.app.appComponent)

代码实现方案一到此就结束了,不知道各位小伙伴有没有get到~接下来让我们来看一看代码实现方案二。

2.4、代码实现方案二:@SubComponent

2.4.1、LoginComponent

@ActivityScope
@Subcomponent(modules = arrayOf(LoginModule::class))
interface LoginComponent {
    @Subcomponent.Builder
    interface Builder {
        @BindsInstance
        fun loginActivity(loginActivity: LoginActivity): Builder
    }
}
复制代码

仅这样还不够,我们还需要改造一下AppComponent,不然我们的Dagger并不知道该如何去实例化LoginComponent。改造很简单,还记不记得我们上文提到按下不表的内容?

OK,就这样我们的LoginComponent已经完成了编写。

2.4.2、LoginActivity

实例化LoginComponent

class LoginActivity {
    private lateinit var loginComponent: LoginComponent

    fun initDagger(appComponent: AppComponent) {
        loginComponent = appComponent
            .loginBuilder() // 我们在AppComponent留下的方法
            .loginActivity(this)
            .build()
   }
    // 省略无关紧要的内容
}
复制代码

此时让我们再来看一下DaggerAppComponent

public final class DaggerAppComponent implements AppComponent {
  	this.loginBuilderProvider =
      		new Factory<LoginComponent.Builder>() {
        		@Override
        		public LoginComponent.Builder get() {
         	 		return new LoginComponentBuilder();
        		}
    	  	};
	}

	private final class LoginComponentBuilder implements LoginComponent.Builder {
		// 省略部分代码
	}
    // 注意理解这里
	private final class LoginComponentImpl implements LoginComponent {
		private LoginComponentImpl(LoginComponentBuilder builder) {
  			assert builder != null;
  			// 初始化LoginComponent具体所提供的依赖
			initialize(builder);
		}
  		// 省略部分代码
	}
}
复制代码

Dagger生成一个LoginComponentImpl,它是DaggerAppComponent的一个内部类。由于内部类可以访问其外部类的成员变量,因此,可以直接从AppComponent访问LoginComponent所需的任何AppComponent依赖项,而无需显式公开这些依赖项。

也就无需像方案一那样:fun schedulerProvider(): BaseSchedulerProvider

对于LoginComponent来说,只要它可以从AppComponent中拿到自己所需的依赖,我们就无需额外的声明。

因此我们再来回过头看看开始贴出来的图:

对于LoginComponent来说,它是AppComponent的内部类,所以可以直接从AppComponent中获取自己所需要的内容。

UserComponent则不同,它需要AppComponent通过明确的方法对外提供依赖,这样UserComponent才可以拿到自己所需的依赖。

三、对比总结

DaggerUserComponent

  • DaggerUserComponent依赖AppComponent。
  • DaggerUserComponent不知道AppComponent的任何实现,只知道通过AppComponent接口公开的依赖项。
  • DaggerUserComponent只能通过schedulerProvider调用AppComponent对外提供的依赖方法。

LoginComponentImpl

  • LoginComponentImpl是AppComponent的子Component。
  • LoginComponentImpl是DaggerAppComponent的内部类。
  • LoginComponentImpl可以直接从AppComponent中拿到自己所需的依赖, 而不需要任何额外的内容。

所以,对于我们来说。当我们不同的Component之间耦合的比较严重时,可以使用@SubComponent;反之我们可以使用方案一:Component依赖。

尾声

到这就接近于尾声了,不知道有多少小伙伴能一路看到这。我在写这个系列的时候一直是“懵懵懂懂”。当写到这一篇的时候,逐渐有了一些豁然开朗的感觉。

扯点题外话,Dagger对于我们Android来说到底好用么?当我们的项目足够大,各种实现足够繁琐时。如果我们拥有较为合理的依赖注入,我们可以非常方便的使用某个对象。举个我们项目中的小例子:

我需要一个接口的实现类,这个接口大概这个样子:

interface IItemAction {
    fun showActionsDialog(activity: Activity,// 省略巨多的参数)
}
复制代码

我如果需要手动new这个IItemAction,我们需要传递它所需要的所有参数,而且有的参数的实例化还需要更多其他的参数...

而如果我们项目拥有较为完善的依赖注入时,我只需要这么做:

@Inject
lateinit var action: IItemAction
复制代码

就酱,这篇文章结束了。

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:咸鱼正翻身

关注下面的标签,发现更多相似文章
评论