“生活如此美好,值得我们为之奋斗。”,我只同意后半句。
本文翻译自 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 实际上是多么简单。
像往常一样进入 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。 但只是在运行时!
如果我们只使用 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,但你也可以称之为 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 能够在我们想要的时候找出任何依赖项的原因,并且仍然确保无论如何都只存在它们的单个实例(如果它们被限定了作用域)。
这实际上是非常方便的。
这是一个单例组件,为了创建一个进程级单例,我们可以使用 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 本身中被追踪。
他们是对的!
所以我甚至不需要深入讨论限定子作用域 —— 虽然我喜欢这个概念,但实际上我们可以避免使用子组件/组件依赖。你通常可以摆脱单例作用域和未作用域的混淆,并且可以从同一个 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
的形式提供数据作为子作用域的依赖关系是非常酷的。 Jake Wharton 多年来一直致力于此,我们仍然只是在追赶。
通常,我们可以轻松地避免使用子作用域,通过保持无作用域的依赖的存活,而不是为组件限定子作用域然后保持子作用域组件的存活。
但是对于子作用域,你可以使用 @Subcomponent 和 @Component(dependencies = {...})进行试验,它们执行相同的操作:从父级继承提供者。
但是 Dagger-Android 呢?
虽然它允许你使用分层查找自动注Activities/Fragments,但设置起来非常神奇。 它并不是那么得可配置;如果我确实想要限定作用域,我个人似乎无法弄清楚如何创建作用域。
所以我不使用它,我个人不建议使用它。 虽然有一些教程可以让 @ContributesXInjector 为(对此)好奇的人工作。
希望这能帮助你了解使用 Dagger 比人们谈论的噩梦般的“学习曲线”简单得多。
这不是那么难,不是吗?