依赖注入
我们知道在一个类中,通常会定义其他类型的变量,这个变量就是我们所说的“依赖“。
对一个类的变量进行初始化,有两种方式。第一种,这个类自己进行初始化;第二种,其他外部的类帮你进行初始化。
| 关键术语 | 含义 | 在 Dagger 2 中的体现 |
|---|---|---|
| 依赖(Dependency) | 类内部需要使用的外部对象 | 任何要注入的字段 / 构造函数参数 |
| 控制反转(IoC) | 对象创建权从类自身“反转”到外部容器 | 由 Dagger 生成的工厂 / Component 负责创建与注入 |
| 依赖注入(DI) | 通过 IoC 将依赖“注入”到目标对象 | 字段注入、构造函数注入、方法注入 |
| @Inject | 标记依赖接收点或可提供实例的构造函数 | @Inject lateinit var repo: Repository @Inject constructor(api: ApiService) |
| @Module / @Provides | 声明如何创建复杂对象 | @Module class NetModule {@Provides fun okHttp() = OkHttpClient()} |
| @Component | 桥梁:连接依赖提供者(Module)与注入目标 | DaggerAppComponent.create().inject(this) |
| 作用域(如 @Singleton) | 限定实例生命周期 | 同一 Component 内共享单例对象 |
1 | 为什么要依赖注入?
-
解耦
- 类只关心“需要什么”,不再关心“如何创建”
-
可测试性
- 依赖可在测试中轻松替换为 Mock
-
可维护性 / 复用
- 复杂依赖关系集中声明,变更影响面小
2 | Dagger 2 工作流程
A(声明依赖<br>@Inject/@Module) --> B(编译期 APT)
B --> C(生成 *DaggerXxx* 工厂/图)
C --> D(运行时调用<br>Component.build())
D --> E(自动 new 对象并注入)
- 编译期 APT 根据注解生成代码 → 0 反射、性能佳
- 运行期 只负责调用生成好的工厂,完成注入
3 | 最小示例
// 依赖类
//Dagger 编译器扫描到这个 `@Inject` 构造函数,就知道要“造一个 `NetworkClient`”时,就调用这个构造函数。
// 它会先满足参数列表里所有依赖(`ApiService`、`Logger` 之类),再去 new `NetworkObject(...)`。
class NetworkClient @Inject constructor(ApiService apiService, Logger logge) // 构造函数注入
// 注入目标
class MainActivity : AppCompatActivity() {
@Inject lateinit var client: NetworkClient // 字段注入
override fun onCreate(saved: Bundle?) {
super.onCreate(saved)
DaggerAppComponent.create().inject(this) // 完成注入
}
}
// Module(可选:当构造函数太复杂时提供)
@Module
class AppModule {
@Provides fun baseUrl(): String = "https://api.xxx.com"
}
// Component
@Singleton
@Component(modules = [AppModule::class])
interface AppComponent {
fun inject(activity: MainActivity)
}
4 | 作用域与单例
| 作用域注解 | 效果 | 何时使用 |
|---|---|---|
@Singleton | Component 级单例 | 全局共享,如 OkHttp、数据库 |
自定义作用域(如 @ActivityScope) | 自定义生命周期 | 与 Subcomponent / Component dependencies 配合 |
注意:作用域生效的前提是:注解要同时出现在
1)@Component、2)@Module or @Provides、3)注入目标三处之一;且依赖必须由同一 Component 管理。
5 | 常见问题速答
| 问题 | 简答 |
|---|---|
| 为什么 @Provides 方法/Module/Component 都要加同一作用域? | 让 Dagger 在编译期生成的工厂知晓“该对象应共享”,否则默认每次 new。 |
能用多态 (BaseActivity) 作为 inject 参数吗? | 不行。Dagger 以“具体类型”生成代码,需写 void inject(MainActivity a)。 |
| 局部 vs 全局单例 | Component 在 Activity 内创建 → 局部 Component 在 Application 创建并持有 → 全局 |
| Dagger 2 与 Hilt | Hilt = Google 官方对 Dagger 的再封装,自动生成 Module/Component,简化模板代码。 |
一句话总结
Dagger 2 = 用编译期代码生成 (APT) 的“依赖工厂”,通过
@Inject+@Module/@Provides+@Component三件套,把“创建依赖”这件事彻底交给框架,既性能友好,又易维护。
整个dagger2其实就是解决了为对象通过APT技术进行赋值的操作。
Dagger2的基本使用
首先先确定我们需要的元素有哪些:
Dagger 2 注入流程一图 & 三要素
-
Module:告诉 Dagger「我会如何创建 A、B、C 这些依赖」
-
Component:告诉 Dagger「把 A、B、C 串成一棵树/图,并在 inject(target) 时分发给目标」
-
Injection Target:告诉 Dagger「我需要这些依赖,请帮我注入」
┌────────────┐ ┌─────────────┐ ┌────────────┐
│ Module(s) │ ──▶ │ Component │ ──▶ │ Target(s) │
│ @Provides │ │ @Component │ │ @Inject │
└────────────┘ └─────────────┘ └────────────┘
▲ │
│ 创建依赖对象 │字段 / 构造函数注入
└────────── 依赖实例被注入 ────────────────┘
| 角色 | 作用 | 样例 |
|---|---|---|
| Object | 最终要注入的依赖 | DataObject、NetworkObject |
| @Module | 告诉 Dagger 如何 创建依赖 | @Module class NetworkModule { @Provides fun net() = NetworkObject() } |
| @Component | 桥梁 —— 连接 Module 与注入目标 | @Component(mods=[NetworkModule::class,DataModule::class]) interface MyComponent { fun inject(activity: MainActivity) } |
核心步骤
-
声明依赖类
class DataObject {} class NetworkObject {} -
提供依赖
@Module class DataModule { @Provides DataObject provideData() { return new DataObject(); } } @Module class NetworkModule { @Provides NetworkObject provideNet() { return new NetworkObject(); } } -
建立组件
@Component(modules = {DataModule.class, NetworkModule.class}) interface MyComponent { void inject(MainActivity a); void inject(MainActivity2 a); } -
在目标类中标记注入点
DaggerMyComponent其实就是 Dagger 在编译期帮我们“生成”出来的MyComponent接口的实现类。它是由 Dagger 的注解处理器(dagger-compiler)在编译阶段自动生成的,名字约定为Dagger+ 你的 Component 接口名。
public class MainActivity extends AppCompatActivity { @Inject NetworkObject net1; @Inject NetworkObject net2; protected void onCreate(Bundle s) { super.onCreate(s); DaggerMyComponent.create().inject(this); } }
为什么 inject 方法参数不能用父类或接口 ?
| 原因 | 说明 |
|---|---|
| 编译期代码生成 | Dagger 在 APT 阶段为 确切类型 生成注入代码,需要固定、完整的类信息。 |
| 类型擦除 | 若用父类 / 接口,真实子类信息被擦除,编译器无法定位具体依赖图。 |
| 依赖解析一致性 | 不同子类所需依赖可能不同,用基类会使 Dagger 无法确定注入内容,失去编译期安全。 |
结论:
void inject(MainActivity activity)必须写 具体类;若多个子类需要注入,就为每个子类写一个injectXXX(),或使用 Subcomponent/Hilt 等更高层方案来复用。
在具体的类中进行注入:
public class MainActivity extends AppCompatActivity {
// 下边就是普通的方式,直接通过new方式生成一个对象。
// NetworkObject networkObject = new NetworkObject();
// NetworkObject networkObject2 = new NetworkObject();
// 这下边就是通过Dagger2的注入方式来实现对象的生成。
@Inject
NetworkObject networkObject;
@Inject
NetworkObject networkObject2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 方式一
// DaggerMyComponent.create().injectMainActivity(this);
// 方式二 使用注解处理器生成代码的细节,来完成的
DaggerMyComponent.builder()
.networkModule(new NetworkModule())
.dataModule(new DataModule())
.build()
// 到这里,初始化了module和component
.injectMainActivity(this);
// 不一样的hashCode 答:非单例
Log.i("zxc", networkObject.hashCode() + " MainActivity"); // 不一样的hashCode
Log.i("zxc", networkObject2.hashCode() + " MainActivity");
}
public void click(View view) {
startActivity(new Intent(this, MainActivity2.class));
}
}
Dagger2生成对象的过程中,有两种方式,均在注释中。两种方式的区别在于,第二种可以配置更多的生成细节。
如何在Dagger中生成单例?
在 Dagger 2 中生成「单例」的两种做法
Dagger 单例两种场景
| 目标 | 关键思路 | 典型做法(见下方代码块) |
|---|---|---|
| 局部单例 | 同一 Component + 同一作用域注解 | ↓ 代码块 ① |
| 全局单例 | Application 启动时只创建一次 Component,并对外暴露 getter | ↓ 代码块 ② |
代码块 ① —— 局部单例 (同一 Component 内共享)
// NetworkModule.java
@Singleton
@Module
class NetworkModule {
@Provides @Singleton
NetworkObject provideNet() {
return new NetworkObject();
}
}
// MyComponent.java
@Singleton
@Component(modules = {NetworkModule.class})
interface MyComponent {
void inject(MainActivity a);
}
- 作用域
@Singleton同时标注在 Module / @Provides / Component。 - 只要依赖通过 同一个
MyComponent注入,得到的NetworkObject都是同一实例;换一个 Component 就会重新创建。
代码块 ② —— 全局单例 (整个 App 进程共享)
// 1️⃣ Application 持有唯一 Component
@HiltAndroidApp // 若不用 Hilt,可手动 new DaggerAppComponent
public class MyApplication extends Application {
private MyComponent appComponent;
@Override public void onCreate() {
super.onCreate();
appComponent = DaggerMyComponent // 依然是局部单例配置
.builder()
.networkModule(new NetworkModule())
.build(); // ← 只创建一次
}
public MyComponent getAppComponent() {
return appComponent;
}
}
// 2️⃣ Activity / Fragment 中使用
public class MainActivity extends AppCompatActivity {
@Inject NetworkObject net1;
@Inject NetworkObject net2;
@Override protected void onCreate(Bundle b) {
super.onCreate(b);
((MyApplication) getApplication())
.getAppComponent() // 全局唯一 Component
.inject(this); // 完成注入
Log.i("TAG", "hashCodes → " + net1.hashCode() + ", " + net2.hashCode());
}
}
NetworkObject依旧用@Singleton声明;- 因为 整个应用只构造一次
DaggerMyComponent,@Singleton对象自然变成“全局单例”。 - 任意 Activity / Fragment 只要通过同一个
appComponent注入,拿到的都是同一NetworkObject。
⚠️ 小结
- 局部单例:限定在某个 Component 图内复用。
- 全局单例:把那个“局部单例 Component” 提升到 Application,并只实例化一次。
生成局部单例
只需要在上述的代码中做以下修改即可:
NetworkModule:
@Singleton // 添加这个单例注解
@Module
public class NetworkModule {
@Singleton // 添加这个单例注解
@Provides
public NetworkObject providerNetworkObject(){
return new NetworkObject();
}
}
MyComponent:
@Singleton // 添加这个单例注解
@Component(modules = {NetworkModule.class, DatabaseModule.class})
public interface MyComponent {
// 注意:这里的参数是不能用多态
void injectMainActivity(MainActivity activity);
void injectMainActivity2(MainActivity2 activity2);
}
总共有三个地方需要添加单例的注解,NetworkModule有两处,MyComponent有一处,但需要注意的是,这个只是局部单例,也就是在另外的Activity中,通过这种方式注入的对象,依然是一个新对象。
生成全局单例
生成全局单例需要在局部单例的基础上,再增加以下逻辑:
在自定义的 Application 中:
public class MyApplication extends Application {
private MyComponent myComponent;
@Override
public void onCreate() {
super.onCreate();
// 就是把之前Activity的代码,拿上来了而已
myComponent= DaggerMyComponent.builder()
.httpModule(new HttpModule())
.databaseModule(new DatabaseModule())
.build();
}
public MyComponent getAppComponent(){
return myComponent;
}
}
然后再使用时,用下边的方式注入:
// 用MyApplication == 全局单例
((MyApplication)getApplication())
.getAppComponent()
.injectMainActivity(this);
使用全局单例的情况下,变成了以下的结构:
现在的Activity的整体代码:
public class MainActivity extends AppCompatActivity {
@Inject
NetWorkObject netWorkObject;
@Inject
NetWorkObject netWorkObject2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 方式一
// DaggerMyComponent.create().injectMainActivity(this);
// 方式二
/*DaggerMyComponent.builder()
.httpModule(new HttpModule())
.databaseModule(new DatabaseModule())
.build()
// 到这里,初始化了module和component
.injectMainActivity(this);*/
// 方式三 用MyApplication == 全局单例
((MyApplication)getApplication())
.getAppComponent()
.injectMainActivity(this);
Log.i("zxc", netWorkObject.hashCode() + " MainActivity");
Log.i("zxc", netWorkObject2.hashCode() + " MainActivity");
}
public void click(View view) {
startActivity(new Intent(this, MainActivity2.class));
}
}
构造函数注入
“构造函数注入”并不是“返回一个构造函数”,而是一种告诉 Dagger “我希望你通过这个构造函数来创建我这个类的实例” 的方式。
一、什么是构造函数注入?
在你要注入的类上,直接在它的构造函数前加上 @Inject,例如:
public class NetworkObject {
@Inject
public NetworkObject(ApiService apiService, Logger logger) {
// Dagger 会自动给你传入 apiService、logger
}
}
背后的含义
- Dagger 编译器扫描到这个
@Inject构造函数,就知道要“造一个NetworkObject”时,就调用这个构造函数。 - 它会先满足参数列表里所有依赖(
ApiService、Logger之类),再去 newNetworkObject(...)。
二、与 Module + @Provides 的区别
-
Module +
@Provides@Module class NetworkModule { @Provides NetworkObject provideNetworkObject(ApiService api, Logger log) { return new NetworkObject(api, log); } }你手写工厂方法,告诉 Dagger “如果要
NetworkObject,就调用我这个方法”。 -
构造函数注入
public class NetworkObject { @Inject public NetworkObject(ApiService api, Logger logger) { … } }你不自己写工厂方法,Dagger 在编译期自动为你生成相当于上面
provideNetworkObject()的逻辑。
三、使用场景
-
参数都是其他依赖(也都能被 Dagger 提供)时,推荐用构造函数注入:
- 代码更少,模块更干净。
-
有特殊构造逻辑、或者你需要用到第三方库的类型,无法改它的构造函数时,才用
@Module+@Provides。
四、底层原理
-
Dagger 的注解处理器在编译期间会为每个带
@Inject构造函数的类生成一个 provider:// 伪代码,编译器生成 Provider<NetworkObject> networkObjectProvider = () -> new NetworkObject(apiServiceProvider.get(), loggerProvider.get()); -
在 Component 的实现里,它就直接调用这一段代码来实例化并注入。
五、示例:省掉 Module
假设 ApiService、Logger 也各自有 @Inject 构造函数,或者通过 Module 提供,那么你只需:
public class ApiService {
@Inject public ApiService(/* ... */) { }
}
public class Logger {
@Inject public Logger() { }
}
public class NetworkObject {
@Inject public NetworkObject(ApiService api, Logger log) { … }
}
@Component
interface AppComponent {
void inject(MainActivity activity);
}
然后在 MainActivity:
@Inject NetworkObject networkObject;
@Override
protected void onCreate(...) {
DaggerAppComponent.create().inject(this);
// networkObject 已经通过构造函数注入并初始化完毕
}
这样一来,你完全不用写额外的 Module,就能把所有依赖串起来,Dagger 会自动调用那些 @Inject 构造函数去“造”对象。
学后测验
一、单选题(每题 4 分)
- 依赖注入(DI)的核心目的 是为了
A. 提高应用启动速度
B. 将对象创建职责从类内部转移到外部,降低耦合
C. 实现多线程并发
D. 简化 UI 绘制
解析:DI/控制反转本质是把“依赖的初始化”交给外部容器,实现解耦。 - 在 Dagger 2 中,
@Inject常用于
A. 标记 Module 类
B. 标记需要注入的字段或构造函数
C. 标记 Component 接口
D. 声明作用域
解析:@Inject既可放字段也可放构造函数,告诉 Dagger 该依赖需要被注入/创建。 - 下列关于
@Module与@Provides的说法,错误的是
A.@Provides方法必须返回 void
B.@Provides方法可以带参数,由 Dagger 提供这些参数
C.@Module类负责告诉 Dagger 如何构造对象
D. 一个 Component 可引用多个 Module
解析:@Provides必须返回实例,返回 void 无意义。 - 若想让
NetworkObject在同一个 Component 生命周期内保持单例,应在 哪些地方 同时加@Singleton(或自定义作用域)?
A. Component 接口、对应 Module 类、以及提供该对象的@Provides方法
B. 只有 Component
C. 只有 Module
D. 只在字段上
解析:三处缺一不可,才能保证作用域一致。 - 以下哪种注入写法 不能通过编译(假设
MyActivity继承BaseActivity)?
A.void injectMainActivity(MyActivity a);
B.void injectMainActivity(BaseActivity a);
C.void injectMainActivity2(MyActivity2 a);
D.void injectMainActivity3(MyActivity3 a);
解析:Dagger 生成代码按“确切类型”生成,不支持在 Component 中用多态参数。 - 对于
@Component.Builder链式构建方式,以下哪句描述正确?
A. 可在 builder 中显式传入自定义 Module 实例
B. 只能使用create(),不能自定义 builder
C. builder 只能绑定一个 Module
D. builder 不能设置作用域
解析:Builder 提供xxxModule(new XxxModule())等方法,灵活传参。
二、判断题(每题 3 分)
- (√)Dagger 2 的依赖解析与代码生成发生在编译期,而非运行期。
解析:APT 会在 compile 时生成DaggerXxx类。 - (×)若两处
@Inject字段属于同一 Component,Dagger 默认会返回同一实例(单例)。
解析:若未声明作用域,Dagger 默认 每次都会 new,不是单例。 - (√)在 Android 中,将 Component 放到自定义 Application 并暴露 getter,可实现“全局单例”。
解析:同一 Application 只实例化一次 Component,其作用域对象在整个应用进程内保持唯一。 - (×)
@Provides方法必须是public。
解析:可为internal/package-private,只要 Component 能访问即可。
三、多选题(每题 5 分)
- 在 Dagger 2 中,下列属于 典型依赖注入入口(Injection Target) 的有
A. Activity/Fragment 字段
B.@Module中的@Provides方法
C. 自定义类中的@Inject字段
D. 构造函数上加@Inject的类实例
解析:只要类上有@Inject字段或@Inject构造函数,都可被注入;@Provides只是供应方。 - 若一个对象既被标注
@Singleton,又被多个 不同 Component 引用,而这些 Component 不是同一个依赖图,则
A. 每个 Component 仍会各自持有一份独立单例
B. 可以通过@Component(dependencies = …)方式让它们共享同一实例
C. 若不做依赖关系声明,跨 Component 不会自动共享单例
D. Application 内永远只有一个该类型实例
解析:@Singleton受 Component 作用域限制;不同组件若互不依赖,各自生成;通过 dependencies/subcomponent 才能共享。
四、简答题(共 20 分,答案要点即可)
13 (10 分)
何谓“控制反转(IoC)”?请结合 Dagger 2 描述它如何体现 IoC。
参考要点
- IoC:将对象创建/依赖解析的控制权,从类自身反转到外部框架(容器)。
- 在 Dagger 2 中,对象不再
new依赖,而是 声明依赖(@Inject) -> 框架在编译期生成工厂 -> 运行时由 Component 注入。开发者只关注“声明”,不关心“构造”;控制权被交给 Dagger,体现 IoC。
14 (10 分)
说明 Dagger 2 如何实现“局部单例”与“全局单例”。请给出关键步骤。
参考要点
-
局部单例
- 给
@Provides方法、Module 类、Component 同时加同一作用域注解(如@Singleton)。 - 只要对象通过同一个 Component 注入,在该 Component 生命周期内都是同一实例。
- 给
-
全局单例
- 将 Component 实例化放在 Application,并保存为成员。
- 所有 Activity/Fragment 通过
((App).component).inject()获取依赖。 - 因为整个应用只创建一次 Component,同作用域对象全局唯一。