失踪的指南:如何使用 Dagger 2(That Missing Guide: How to use Dagger2)

“生活如此美好,值得我们为之奋斗。”,我只同意后半句。

本文翻译自 Gabor Varadi 的 That Missing Guide: How to use Dagger2,主要介绍了使用 Dagger2 时需要注意的地方。

目录

步骤:

如何将 Dagger 添加到你的项目中

如何判断你做得不对

让我们来看一个真实的例子吧

使用构造函数 injection(注入)+ scope(作用域)

@Component:真正对我们有意义的东西

创建 component(组件)的实例

...但我想要一个不是单例的 presenter

结论


Dagger 是一个依赖注入框架,可以更轻松地管理应用程序中类之间的依赖关系。 如果你之前从未听说过 dependency injection(依赖注入),请查看 Dagger2 团队成员 /u/ronshapiro 的快速 ELI5

在大约两年半以前 Dagger2 问世的那一天,Google 创建了原始 Dagger(由 Square 创建)的分支,这让我很兴奋。

最初的 Dagger 部分基于反射,并且和 Proguard 兼容的并不好。然而,Dagger2 完全基于注解处理,因此它在编译时具有魔力。它没有任何额外的 Proguard 配置,并且一般更快。

但但但但但是,尽管文档越来越好,但他们仍然主要是关于用热虹吸管和加热器冲泡咖啡(dagger的文档示例是关于咖啡的)的。 我从来没有在真正的 app 中出现过其中任何一种(作者不喜欢官方文档致力于用煮咖啡来介绍 Dagger 的姿势?!@#&。。)。

因此,Koin 和 Kodein 以及其他随机新兴库试图宣称他们是“最简单的 DI 解决方案”,使“依赖注入无痛”,我认为是时候编写一个(或另一个?)快速指南,向你展示使用 Dagger2 实际上是多么简单。

步骤:

  • 完全忽略 dagger-android(在2.10中添加),你不需要它
  • 尽可能地使用被 @Inject 注解的构造函数(以及类中附带的作用域(scope))
  • 使用 @Component 和 @Module(当你真正需要它时)
  • 理解 @Singleton 作用域(基本上“只允许创建此事件的一个实例,请”),并且所有作用域说的是同一种东西。
  • 当你实际有需要创建子作用域的架构设计时,才开始使用子作用域。 你通常可以在没有他们的情况下离开。 无论哪种方式,都不要强调他们。

 

如何将 Dagger 添加到你的项目中

像往常一样进入 build.gradle dependencies

annotationProcessor ‘com.google.dagger:dagger-compiler:2.16.1’
implementation ‘com.google.dagger:dagger:2.16.1’
compileOnly ‘org.glassfish:javax.annotation:10.0-b28’

当然,使用Kotlin,用kapt替换annotationProcessor(并且不要忘记 apply plugin:'kotlin-kapt')。

 

如何判断你做得不对

其实很久以前我就犯过这个错误,很多指南也犯了同样的错误。事实上,我之所以认为这是错误的,是因为有一篇日本文章提到了这一点,所以就是这样。

假设你有这个: 

public class MyServiceImpl implements MyService {
    private final ApiService apiService;
    public MyServiceImpl(ApiService apiService) {
        this.apiService = apiService;
    }
}

 

这是你不要做的事情:

@Module
public class ServiceModule {
    @Provides
    @Singleton
    public MyService myService(ApiService apiService) {
        return new MyServiceImpl(apiService); // <-- NO
    }
}

相反,假设你真的需要将实现绑定到接口,那么这就是你做的:

@Module
public class ServiceModule {
    @Provides
    MyService myService(MyServiceImpl service) {
        return service; // <-- YES
    }
}
@Singleton // <-- YES
public class MyServiceImpl implements MyService {
    private final ApiService apiService;
    @Inject // <-- YES
    public MyServiceImpl(ApiService apiService) { // <-- YES
        this.apiService = apiService;
    }
}

好的,还有一件事可以轻松简化这一切。 不,我不是指使用 @Binds(它可以做同样的事情,除了它是一个抽象的方法)

如果我不需要一个类的接口,因为我在运行时没有它的多个子类(否则只能模拟用于测试的东西),那么我就不需要接口。那么我就不再需要这个模块了。

@Singleton
public class MyService {
    private final ApiService apiService;
    @Inject
    public MyService(ApiService apiService) {
        this.apiService = apiService;
    }
}

Boom,这是 MyService 的 Dagger 配置。 添加 @Singleton 并添加 @Inject ,它就可以工作了。

我会说这比 Kodein 的 instance( ) 链更好,而且是通过 provider 提供的,对吗?

关于接口的一个警告

作为对一些有效批评的回应,我必须指出,虽然——我不是说你应该删除你曾经编写的每个接口,但是你应该考虑你是否真的需要那个接口。

如果它有助于你的测试,例如配置不同的数据源,那么一定要使用接口! 无论如何,Kotlin 使绑定方法成为单一的内衬。

@Provides fun myService(impl: MyServiceImpl) : MyService = impl;

让我们来看一个真实的例子吧

我想下载一批猫,并在 RecyclerView 的屏幕上显示它。 当我向下滚动时,我想下载更多猫,并将它们保存在至少(能保证条目)轮换的数据库中。

问题:一个手动DI的世界,没有DI框架

通过足够的敲打(键盘),我们最终得到以下类:

Context appContext;     
CatTable catTable;     
CatDao catDao;     
CatMapper catMapper;     
CatRepository catRepository;     
CatService catService;     
Retrofit retrofit;     
DatabaseManager databaseManager;     
Scheduler mainThreadScheduler;     
Scheduler backgroundThreadScheduler;

在我们的应用程序中,我们可以手动设置这些类,就像这样:

this.appContext = this;
this.mainThreadScheduler = new MainThreadScheduler();
this.backgroundThreadScheduler = new BackgroundScheduler();
this.catTable = Tables.CAT;
this.databaseManager = new DatabaseManager(
                         appContext, Tables.getTables());
this.catMapper = new CatMapper();
this.retrofit = new Retrofit.Builder()
       .baseUrl("http://thecatapi.com/")
       .addConverterFactory(SimpleXmlConverterFactory.create())
       .build();
this.catService = retrofit.create(CatService.class);
this.catDao = new CatDao(catTable, catMapper, databaseManager);
this.catRepository = new CatRepository(catDao, catService, 
    catMapper, backgroundThreadScheduler, mainThreadScheduler);

这些只是获取一批猫的简单类……然而,这需要很多手动配置。

这些实例化的顺序很重要! 如果你在创建 dao、服务或映射器之前意外尝试创建 NPE(空指针异常),那么该存储库将抛出 NPE。 但只是在运行时!

使用构造函数 injection(注入)+ scope(作用域)

如果我们只使用 Dagger2 和构造函数注入,就可以使这更容易。为了保持简短,让我们假设(使用)Kotlin(还没上 kotlin 车的同学要抓紧了)。

@Singleton
class CatMapper @Inject constructor() {
}
@Singleton
class CatDao @Inject constructor(
    private val catTable: CatTable,
    private val catMapper: CatMapper,
    private val databaseManager: DatabaseManager) {
}
@Singleton
class CatRepository @Inject constructor(
    private val catDao: CatDao,
    private val catService: CatService,
    private val catMapper: CatMapper,
    private @Named("BACKGROUND") val backgroundThread: Scheduler, 
    private @Named("MAIN") val mainThread: Scheduler) {
}

我们可以自由地使用我们在构造函数中收到的所有东西,它们都是可用的,并且是从外部提供的。 Dagger将弄清楚“如何(使用)”。

提供没有 @Inject 构造函数的东西

我们可以将构造函数注入用于我们所拥有的东西,这很棒,但是对于用 builders(构建器) 构建的或者从其他地方构建的东西呢?

这正是 modules(模块)的用途。

在这种情况下,Retrofit 和 Retrofit 创建的实现。

@Module
class ServiceModule {
    @Provides
    @Singleton
    fun retrofit(): Retrofit = Retrofit.Builder()
       .baseUrl("http://thecatapi.com/")
       .addConverterFactory(SimpleXmlConverterFactory.create())
       .build();
    @Provides
    @Singleton
    fun catService(retrofit: Retrofit): CatService = 
         retrofit.create(CatService::class.java);
}

好的,现在 Dagger 知道如何提供这些东西,我们可以直接在构造函数注入中使用它们。 (代码)很整洁。

提供现有实例

假设我们也想要 application context。 我们想要提供这个,但我们不拥有它,对吧? 所以我们需要一个 module。

@Module
class ContextModule(private val appContext: Context) {
   @Provides
   fun appContext(): Context = appContext;
}

val contextModule = ContextModule(this); // this === app

Boom,完成了。

可以使用 @BindsInstance 并手动定义你自己的 @Component.Builder,以三倍的配置(量)来做实际上完全相同的事(提供外部提供的 Context),但由于它执行相同的操作,并且更加复杂,因此我们实际上不需要关心。只需使用带有构造函数参数的 module。

提供命名实现

在我们的示例中,我们不能避免使用 BackgroundScheduler 和 MainThreadScheduler 作为 Scheduler 的实例(以允许传入 ImmediateScheduler),因此我们也必须为此添加一个 module。

@Module
class SchedulerModule {
    @Provides
    @Singleton
    @Named("BACKGROUND")
    fun backgroundScheduler(): Scheduler = BackgroundScheduler();
    @Provide
    @Singleton
    @Named("MAIN")
    fun mainThreadScheduler(): Scheduler = MainThreadScheduler();
}

@Component:真正对我们有意义的东西

我们已经用 构造函数注入 和 模块 描述了如何创建事物。 现在我们需要能够制造东西的东西。 提供者,工厂的集合,你可以这么叫它。 它被称为 @Component,但你也可以称之为 ObjectGraph,是一样的东西。

我绝对不会叫它NetComponent(因为这没有任何意义),我会叫它 SingletonComponent。

它看起来像这样:

@Component(modules={ServiceModule.class, 
                    ContextModule.class,
                    SchedulerModule.class})
@Singleton
public interface SingletonComponent {
    // 这里没有任何东西
}

超级复杂,对吧? 它的问题在于,除非我们提供允许对具体类型进行变量注入的方法,或者提供方法,否则我们无法从组件中获得任何内容。

所以你可以添加

void inject(MyActivity myActivity);

或者

Context appContext();
CatDao catDao();
...

我个人认为,变量注入的规模相当严重(太多了),所以我更喜欢为我需要的每个依赖项添加一个提供方法,然后传递我的组件。 所以它看起来像这样。

@Component(modules={ServiceModule.class, 
                    ContextModule.class,
                    SchedulerModule.class})
@Singleton
public interface SingletonComponent {
    Context appContext();
    CatTable catTable();
    CatDao catDao();
    CatMapper catMapper();
    CatRepository catRepository();
    CatService catService();
    Retrofit retrofit();
    DatabaseManager databaseManager();
    @Named("MAIN") Scheduler mainThreadScheduler();
    @Named("BACKGROUND") Scheduler backgroundThreadScheduler();
}

这看起来像是一大堆方法,但这就是为什么 Dagger 能够在我们想要的时候找出任何依赖项的原因,并且仍然确保无论如何都只存在它们的单个实例(如果它们被限定了作用域)。

这实际上是非常方便的。

创建 component(组件)的实例

这是一个单例组件,为了创建一个进程级单例,我们可以使用 Application.onCreate( ) 作为我们的选择。

class CustomApplication: Application() {
    lateinit var component: SingletonComponent;
    override fun onCreate() {
        super.onCreate();
        component = DaggerSingletonComponent.builder()
                       .contextModule(ContextModule(this))
                       .build();
    }
}

DaggerSingletonComponent 是由 Dagger 自己创建的,它是我们需要为正确的 DI 编写的一堆代码。 框架为我们“写”了它。

现在,如果我们有一个 Application 类的引用,我们可以检索任何完全注入的类 - 或者使用它来注入我们的 activities/fragments(由系统使用它们的无参构造函数创建)。

访问组件,注入 Activities/Fragments

我喜欢的一个技巧是使进程可以从任何地方访问,甚至没有 application 实例。

class CustomApplication: Application() {
    lateinit var component: SingletonComponent
        private set
    override fun onCreate() {
        super.onCreate()
        INSTANCE = this
        component = DaggerSingletonComponent.builder()
                       .contextModule(ContextModule(this))
                       .build()
    }
    companion object {
       private var INSTANCE: CustomApplication? = null
       @JvmStatic
       fun get(): CustomApplication = INSTANCE!!
    }
}

然后我就能做到

class Injector private constructor() {
    companion object {
        fun get() : SingletonComponent =
           CustomApplication.get().component;
    }
}

所以现在在我的 Activity 中,我可以做到

class CatActivity: AppCompatActivity() {
    private val catRepository: CatRepository;
    init {
        this.catRepository = Injector.get().catRepository();
    }
}

有了它,我们实际上可以在 Activities 中得到任何我们想要的东西。 人们可能会说“嘿,等等,你依赖于服务查找”,但我认为这是你可以为 Activity/Fragment 做的 最好/最稳定 的事情。

对于你实际拥有的类而言,这是最好的选择,那么为什么我要使用服务查找呢? 只需使用 @Inject 即可!

 

...但我想要一个不是单例的 presenter

那么我们有三个选择:

  • 创建一个无作用域的 presenter,当配置更改时让它随着 Activity/Fragment 销毁。
  • 创建一个无作用域的 presenter 并将其保留在 Activity/Fragment 的作用域内。
  • 创建有作用域的子组件和子作用域的 presenter。

有人可能会争辩说,不需要在配置更改中保存你的的 presenter,只需观察由网络调用引起的更改,这些更改在单例业务逻辑中被追踪,而不是在 presenter 本身中被追踪。

他们是对的!

所以我甚至不需要深入讨论限定子作用域  —— 虽然我喜欢这个概念,但实际上我们可以避免使用子组件/组件依赖。你通常可以摆脱单例作用域和未作用域的混淆,并且可以从同一个 Injector 中获得它们,而不必在其作用域期间对子作用域组件进行切换。

I must admit that creating subscoped components with the right lifecycle is easy now, you’d just need to do it in ViewModelProviders.Factory for ViewModel.

我必须承认,现在使用正确的生命周期创建子作用域组件很简单,只需要在 ViewModel 的 ViewModelProviders.Factory 中做这项工作即可。

不管怎样,这是一个没有作用域的 presenter:

// UNSCOPED
class CatPresenter @Inject constructor(
    private val catRepository: CatRepository 
) {
   ...
}

这样,我们的 Activity 就可以获得这个实例......如果我们想要在配置更改中保留它,也不是那么难:

class CatActivity: AppCompatActivity() {
    private lateinit var catPresenter: CatPresenter;
    override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState);
         val nonConfig = getLastCustomNonConfigurationInstance();
         if(nonConfig != null) {
             this.catPresenter = nonConfig as CatPresenter;
         } else {
             this.catPresenter = Injector.get().catPresenter();
         }
    }
    override fun onRetainCustomNonConfigurationInstance() =  
        catPresenter;
}

这样,虽然 presenter 本身没有作用域,但它只在实际需要时才创建。 我们还可以使用新发布的 Architecture Components 中的 ViewModel。 我们不需要为它创建一个子组件。

但是子作用域呢?

如果你仍然对使用子作用域和 @Subcomponent 感兴趣,请随时查看这个使用 Dagger2 修改过的 Mortar 例子

以 Observable<T> 的形式提供数据作为子作用域的依赖关系是非常酷的。 Jake Wharton 多年来一直致力于此,我们仍然只是在追赶。

通常,我们可以轻松地避免使用子作用域,通过保持无作用域的依赖的存活,而不是为组件限定子作用域然后保持子作用域组件的存活。

但是对于子作用域,你可以使用 @Subcomponent 和 @Component(dependencies = {...})进行试验,它们执行相同的操作:从父级继承提供者。

但是 Dagger-Android 呢?

虽然它允许你使用分层查找自动注Activities/Fragments,但设置起来非常神奇。 它并不是那么得可配置;如果我确实想要限定作用域,我个人似乎无法弄清楚如何创建作用域。

所以我不使用它,我个人不建议使用它。 虽然有一些教程可以让 @ContributesXInjector 为(对此)好奇的人工作。

 

结论

希望这能帮助你了解使用 Dagger 比人们谈论的噩梦般的“学习曲线”简单得多。

  • 应用 @Singleton 和 @Inject
  • 在你不能使用 @Inject 的地方创建 @Module
  • 创建 @Component 并使用组件

这不是那么难,不是吗?

猜你喜欢

转载自blog.csdn.net/qq_33404903/article/details/84779550