【从零开始撸一个App】Dagger2
Dagger2是一个IOC框架,一般用于Android平台,第一次接触的朋友,一定会被搞得晕头转向。它延续了Java平台Spring框架代码碎片化,注解满天飞的传统。尝试将各处代码片段串联起来,理清思绪,真不是件容易的事。更不用说还有各版本细微的差别。
与Spring不同的是,Spring是通过反射创建对象的,而Dagger2是[通过apt插件]在编译期间生成代码,这些生成的代码负责依赖对象创建。
本文旨在以简单通俗易懂的方式说明如何使用Dagger2,对其背后设计不做深入探讨。人生苦短,码农更甚,先知其然等有空时再知其所以然,不失为撸App的较好实践。
正式开始前,先给像笔者这样的小白定义几个概念,方便下文理解:
- 依赖对象:比如bean,被其它类所需(依赖)的对象,需要以某种方式注入到目标对象中。
- 目标对象:依赖对象的需求方,注入者将依赖对象注入其中。
- 注入者/[依赖对象的]容器:维护依赖对象,将依赖对象注入到目标对象的工具类。
入门
首先,添加依赖库。
implementation "com.google.dagger:dagger:2.27"
// kapt是服务于Kotlin的Annotation Processing Tool,用于编译时处理注解
kapt "com.google.dagger:dagger-compiler:2.27"
一般来说,IOC会根据规则在运行时自动帮我们生成依赖对象实例。Dagger2提供了两种声明依赖对象的方式:
- 构造函数有
@Inject修饰。 @Module修饰的类中所定义的有@Provides修饰的方法提供(可用于依赖对象是第三方库中的对象)。
// 方式一(注意此处hen也要是依赖对象,否则将为null或者直接报错)
class Egg @Inject constructor(private val hen: Hen)
// 方式二
@Module
class HenModule {
@Singleton
@Provides
fun provideHen() = Hen()
}
大家注意@Singleton注解(javax.inject中定义),它表示该依赖对象的作用域或者说生命周期,Dagger2中可通过@Scope定义。@Singleton是Dagger2默认支持的scope,表示依赖对象是单例。需要注意的是,通常我们将单例保存在一个静态域中,这样的单例往往要等到虚拟机关闭时候,所占用的资源才释放,但是,Dagger通过Singleton创建出来的单例并不保持在静态域上,而是保留在同样标注了@Singleton的Component实例中(依赖对象容器,接下来会讲到)。其实对于任意scope,只要依赖对象和Component标注的是相同scope,那么该依赖对象在相应的Component中就是一个局部单例,仅会调用一次工厂类生成对象实例。一般来说我们只要使用默认的@Singleton即可,没必要自定义,自定义Scope常用于业务或逻辑的划分。
如果不想依赖对象与Component绑定,则可以使用@Reusable作用域。
上面说的Component是@Component注解修饰的接口,Dagger2会先寻找它,以此为入口得到所有依赖对象。该接口中可定义类似void inject(Target target)的方法。显然,@Component注解的接口就是注入者,它将依赖对象和目标对象串联了起来。
@Singleton
@Component(modules = [HenModule::class]) //依赖对象
interface MyAppComponent {
fun inject(activity: MainActivity); //目标对象
}
最后我们就可以在目标对象中愉快地使用依赖对象了。
class MainActivity : AppCompatActivity() {
@Inject
lateinit var hen: Hen
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DaggerMyAppComponent.builder().build().inject(this); // 关键
}
}
改进
如上,对于Activity/Fragment来说,它们的实例化是系统完成的,因此我们只能在它们使用之前的某个环节比如onCreate回调方法内手动将其自身依附到Dagger2中,这产生了至少一个问题:这种方式破坏了依赖注入的核心准则:一个类不应该知道它是如何被注入的。为了解决这个问题,Dagger 2.10版本引入的dagger-android,它是一个专为Android设计的除了Dagger主模块之外的全新模块。
首先,新增两个依赖库。
implementation "com.google.dagger:dagger-android:2.27"
kapt "com.google.dagger:dagger-android-processor:2.27"
针对每个目标类编写对应的子容器代码:
@Subcomponent(modules = [HenModule::class])
interface MainActivitySubcomponent : AndroidInjector<MainActivity?> {
@Subcomponent.Builder
abstract class Builder : AndroidInjector.Factory<MainActivity?>
}
再针对每个目标类编写对应的依赖对象代码:
@Module(subcomponents = MainActivitySubcomponent::class)
abstract class MainActivityModule {
@Binds
@IntoMap
@ActivityKey(MainActivity::class)
abstract fun bindMainActivityInjectorFactory(builder: MainActivitySubcomponent.Builder?): AndroidInjector.Factory<out Activity?>?
}
修改之前的MyAppComponent代码:
@Singleton
@Component(modules = [AndroidInjectionModule::class,MainActivityModule::class]) // 关键,引入AndroidInjectionModule
interface MyAppComponent {
fun inject(app: MyApplication); //注入到Application
}
Application需要继承HasActivityInjector,实现inject方法,返回DispatchingAndroidInjector对象。
class MyApplication : Application(), HasActivityInjector {
// 由dagger.android自动注入
@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
override fun androidInjector() = dispatchingAndroidInjector
override fun onCreate() {
super.onCreate()
DaggerMyAppComponent.builder().build().inject(this)
}
}
创建一个BaseActivity,在super.onCreate之前调用AndroidInjection.inject(this),这样之后的Activity就只需要继承它,就可以使用各自的依赖对象了。
open class BaseActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
}
}
class MainActivity : BaseActivity() {
@Inject
lateinit var hen: Hen
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//DaggerMyAppComponent.builder().build().inject(this); // 不需要了
}
}
可见,为了解决前述问题,引入了更多的代码,复杂度也提高了,这是否有必要,值得商榷。好在Dagger2提供了@ContributesAndroidInjector注解解决了这个问题。
再改进
- 删除前述的MainActivitySubcomponent和MainActivityModule;
- 创建基于BaseActivity类型的容器(非必须):
@Subcomponent
interface ActivityComponet: AndroidInjector<BaseActivity>{
@Subcomponent.Builder
abstract class Builder: AndroidInjector.Builder<BaseActivity>()
}
- 创建ActivityModule,管理所有Activity。注意@ContributesAndroidInjector注解的使用。
@Module(subcomponents = [ActivityComponet::class])
abstract class ActivityModule{
@ContributesAndroidInjector
abstract fun mainActivityInjector(): MainActivity
}
- 修改之前的MyAppComponent代码:
@Singleton
@Component(modules = [AndroidInjectionModule::class,ActivityModule::class]) // MainActivityModule改为ActivityModule
interface MyAppComponent {
fun inject(app: MyApplication);
}
大功告成!我们再也不需要重复地创建XXXActivityModule和XXXActivitySubcomponent类了。
参看Dagger & Android
后记拾遗
@BindsInstance:编译后会在Component的Builder类中生成修饰的方法里面的参数对应的成员变量,so该变量对应的对象可在与该Component相关的Module中通过@Inject注入,可看作是该Component范围内的全局单例,类似于上述的@Scope的作用。
Dagger 2.22 起引入了 @Component.Factory, 可以取代@Component.Builder的使用,Factory在许多场景的上的使用相对于Builder会更简单。
Dagger 2.23新增了一个HasAndroidInjector接口,用于替代HasActivityInjector, HasServiceInjector, HasBroadcastReceiverInjector, HasSupportFragmentInjector四个接口,让Application中的代码更简洁,目前还是beta版。一般如果我们只需要HasActivityInjector的话那也无所谓了。参看Reducing Boilerplate with the new HasAndroidInjector in Dagger 2.23
了解一下javax.inject,这个是 Java EE 6 规范 JSR 330 -- Dependency Injection for Java 中的东西, Spring、Guice兼容该规范。
Assisted Injection:似乎是Guice引入的一个概念?——
Sometimes a class gets some of its constructor parameters from the Injector and others from the caller. 对于这种情况,我们常封装一个工厂类,该类内部提供了注入类型的实例化,对外暴露一个生产方法,该方法只接收需要外部传入的参数。如果觉得手写这种工厂类太过麻烦或工作量太大,那么可以使用AssistedInject自动生成。参看AssistedInject,Assisted Injection for JSR 330
抛弃dagger-android:虽然最终改进之后,代码变得清晰很多,但内在逻辑反而更加复杂了。这种[理解门槛较高的]复杂度就像一颗定时炸弹,让人夜不能寐。当我使用到ViewModel之后发现,也可以不引入dagger-android,而是将所有依赖注入到ViewModel中,再由ViewModel暴露给系统组件。然而由于框架所限,其实不然——ViewModel是由Android框架本身维护的,当然框架也给我们留了一个自定义provider viewmodel的口子,就是ViewModelProvider.Factory——这又是一项颇费脑力的工程,参看How to Inject ViewModel using Dagger 2。这步完成以后,我们再将ViewModelProvider.Factory实例注入到Application中,变为一个全局工厂对象,Activity/Fragment直接拿来用即可,再也不需要与依赖注入有任何瓜葛,自然也不需要dagger-android了。也有大神跟我想到一块,参看当Dagger2撞上ViewModel
参考资料
Dagger2从入门到放弃再到恍然大悟
Dagger2 @Component 和@SubComponent 区别解惑
学习Dagger2笔记
dagger.android(Dagger2中的AndroidInjector)使用解析
Dagger2 中的 Binds、IntoSet、IntoMap
dagger android 学习(一):dagger基础使用
Dagger2在Android平台上的新魔法(作者了解了dagger-android背后的原理后弃用)
【从零开始撸一个App】Dagger2的更多相关文章
- 【从零开始撸一个App】Kotlin
工欲善其事必先利其器.像我们从零开始撸一个App的话,选择最合适的语言是首要任务.如果你跟我一样对Java蹒跚的步态和僵硬的语法颇感无奈,那么Kotlin在很大程度上不会令你失望.虽然为了符合JVM规 ...
- 【从零开始撸一个App】PKCE
一个成功的App背后肯定有一堆后端服务提供支撑,认证授权服务(Authentication and Authorization Service,以下称AAS)就是其中之一,它是约束App.保障资源安全 ...
- 【从零开始撸一个App】RecyclerView的使用
目标 前段时间打造了一款简单易用功能全面的图片上传组件,现在就来将上传的图片以图片集的形式展现到App上.出于用户体验考虑,加载新图片采用[无限]滚动模式,Android平台上我们优选Recycler ...
- 【从零开始撸一个App】Fragment和导航中的使用
Fragment简介 Fragment自从Android 3.0引入开始,它所承担的角色就是显而易见的.它之于Activity就如html片段之于页面,好处无需赘述. Fragment的生命周期和Ac ...
- Android(4)—Mono For Android 第一个App应用程序
0.前言 年前就计划着写这篇博客,总结一下自己做的第一个App,却一直被新项目所累,今天抽空把它写完,记录并回顾一下相关知识点,也为刚学习Mono的同学提供佐证->C#也是开发Android的! ...
- 深入浅出React Native 3: 从零开始写一个Hello World
这是深入浅出React Native的第三篇文章. 1. 环境配置 2. 我的第一个应用 将index.ios.js中的代码全部删掉,为什么要删掉呢?因为我们准备从零开始写一个应用~学习技术最好的方式 ...
- Django1.8教程——从零开始搭建一个完整django博客(一)
第一个Django项目将是一个完整的博客网站.它和我们博客园使用的博客别无二致,一样有分类.标签.归档.查询等功能.如果你对Django感兴趣的话,这是一个绝好的机会.该教程将和你一起,从零开始,搭建 ...
- 从零开始写一个武侠冒险游戏-6-用GPU提升性能(1)
从零开始写一个武侠冒险游戏-6-用GPU提升性能(1) ----把帧动画的实现放在GPU上 作者:FreeBlues 修订记录 2016.06.19 初稿完成. 2016.08.05 增加对 XCod ...
- 从零开始构建一个的asp.net Core 项目
最近突发奇想,想从零开始构建一个Core的MVC项目,于是开始了构建过程. 首先我们添加一个空的CORE下的MVC项目,创建完成之后我们运行一下(Ctrl +F5).我们会在页面上看到"He ...
随机推荐
- CVE-2020-5902 F5 BIG-IP 远程代码执行漏洞
CVE-2020-5902 F5 BIG-IP 远程代码执行漏洞复现 漏洞介绍 F5 BIG-IP 是美国 F5 公司的一款集成了网络流量管理.应用程序安全管理.负载均衡等功能的应用交付平台. 近日, ...
- Scala 面向对象(十三):隐式转换和隐式参数
隐式转换的实际需要=>指定某些数据类型的相互转化 1 隐式函数基本介绍 隐式转换函数是以implicit关键字声明的带有单个参数的函数.这种函数将会自动应用,将值从一种类型转换为另一种类型 隐式 ...
- 数据可视化之DAX篇(五) 使用PowerBI的这两个函数,灵活计算各种占比
https://zhuanlan.zhihu.com/p/57861350 计算个体占总体的比例是一个很常见的分析方式,它很简单,就是两个数字相除,但是当需要计算的维度.总体的范围发生动态变化时,如何 ...
- PowerShell创建参考窗口
背景 平常我们经常遇到这样一个问题,在使用一个窗口工作时常常需要参考其他窗口的文字或图片,此时就需要频繁切换窗口:或者是看视频时需要参考前面进度的画面:或者是阅读或写文档时需要参考其他位置的文字,这时 ...
- easyui获取datagrid中的某一列的所有值
function getCol(){ var rows = $("#dg").datagrid("getRows"); var total = "&q ...
- JVM详解之:java class文件的密码本
目录 简介 一个简单的class ClassFile的二进制文件 class文件的密码本 magic version 常量池 描述符 access_flags this_class和super_cla ...
- Guava的两种本地缓存策略
Guava的两种缓存策略 缓存在很多场景下都需要使用,如果电商网站的商品类别的查询,订单查询,用户基本信息的查询等等,针对这种读多写少的业务,都可以考虑使用到缓存.在一般的缓存系统中,除了分布式缓存, ...
- 【Nginx】如何格式化日志并推送到远程服务器?看完原来很简单!!
写在前面 Nginx作为最常用的反向代理和负载均衡服务器,被广泛的应用在众多互联网项目的前置服务中,很多互联网项目直接将Nginx服务器作为整个项目的流量入口.这就使得我们可以通过对Nginx服务器日 ...
- DirectX11 With Windows SDK--35 粒子系统
前言 在这一章中,我们主要关注的是如何模拟一系列粒子,并控制它们运动.这些粒子的行为都是类似的,但它们也带有一定的随机性.这一堆粒子的几何我们叫它为粒子系统,它可以被用于模拟一些比较现象,如:火焰.雨 ...
- BUUCTF-Web Easy Calc
要素察觉 打开calc.php发现源码 过滤了很多字符.题目一开始提示了有waf,最后通过eval实现计算功能.考虑利用该函数读取flag文件,先尝试弹个phpinfo 被waf拦截,在num参数前面 ...