依赖注入是用于实现控制反转的方式之一,另外一种实现控制反转的方式是依赖查找
- 基于<依赖查找 / 服务发现>
public class Man {
Car car = ServiceManager.get(Car);
public Man() { }
}
常见的依赖注入的实现方式
- 基于构造方法 或 setter 方法
public class Man {
Car car;
public Man(Car car) {
this.car = car;
}
}
public class Man {
public void setCar(Car car) {
this.car = car;
}
}
- 基于注解
public class Man {
@Inject
Car car;
}
对比上述三种方式:方案一将 Car 的构建放到了外部,问题并没有解决,只是进行了转移。方案二由框架来帮助构建实例,和依赖查找的主要区别是依赖查找需要目标对象主动去获取,而依赖注入无需目标对象感知,由框架主动赋值
依赖注入在软件开发中被广泛应用,比较知名的有 Spring、Guice 等。依赖注入在工程上带来的好处主要有:
- 代码复用性更好
- 更容易重构
- 更容易测试
本文要介绍的 Dagger 2 是专门为 Android 开发设计的一款依赖注入框架。在设计上遵循 JSR 330 标准,通过注解生成代码的方式来实现依赖注入。一方面避免了用户创建大量的模版代码,另一面避免了运行时通过反射的方式调用带来的性能问题。同时编译期间的依赖检查,使得问题能够提前暴露,易于追踪
基本用法
- 使用 @Inject 标注需要注入的变量或方法
- 使用 @Inject 标注构造函数来完成实例的创建
- 使用 @Module 来完成实例的创建
- @Component可以标注接口或抽象类,Component 桥梁可以完成依赖注入过程
Lazy & Provider
Lazy<T>类 可以实现延迟注入,降低内存开销(DoubleCheck,缓存 Instance 实例),只在第一次调用时创建实例,但由于 Instance 实例不是静态的,所以不是单例模式
class Monkey @Inject constructor() {
@Inject
lateinit var stick1: Lazy<Stick>
@Inject
lateinit var stick2: Lazy<Stick>
@Test
fun compare() {
assert(stick1.get() == stick2.get()) // false
}
}
Provider<T> 类每次调用都会创建新的实例
Provider 和 Lazy 结合使用:Provider<Lazy<Stick>>,这样既可以保证效率,也可以保证能获取到多个实例
Qualifier
对于同一类型有多个实例需要注入的情况,通过限定符 @Qualifier 可以防止依赖迷失。系统提供了默认的 @Name 限定符,但是不是强类型的,我们也可以通过 Qualifier 来生成自定义的限定符
Scope
相当于单例模式,对于同一个 Component,每次调用 Component.inject 或 Component.getXxx 方法内部维护的都是同一个对象。这引出了 Component 分层的概念,Component 有两种分层方式:继承、依赖(如果不想全部暴露,可以选择依赖关系)
依赖关系 vs 继承关系
- 相同点
- 两者都能复用其他 Component 的依赖
- 有依赖关系和继承关系的 Component 不能有相同的 Scope
- 不同点
- 依赖关系中被依赖的 Component 必须显式地提供公开依赖实例的接口,而 SubComponent 默认继承 parent Component 的依赖
- 依赖关系会生成两个独立的 DaggerXXComponent 类,而在继承关系中,SubComponent 需要声明 @Subcomponent.Builder 标注的 builder 方法,SubComponent 不会生成独立的 DaggerXXComponent 类
// 依赖关系
@CustomScope
@Component(modules = [AnimalModule::class], dependencies = [BaseComponent::class])
interface ActivityComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun bindCow(cow: Cow): Builder
fun bindBaseComponent(baseComponent: BaseComponent): Builder
fun build(): ActivityComponent
}
fun injectDaggerActivity(activity: DaggerActivity)
}
@BaseScope
@Component(modules = [RealPeople::class, PeopleModule::class])
interface BaseComponent {
fun man(): Man
fun woman(): Woman
fun people(): People
}
// 继承关系
@Module(subcomponents = SonComponent.class)
public class CarModule {
@Provides
@ManScope
static Car provideCar() {
return new Car();
}
}
@ManScope
@Component(modules = CarModule.class)
public interface ManComponent {
void inject(Man man);
}
@SonScope
@SubComponent(modules = BikeModule.class)
public interface SonComponent {
void inject(Son son);
@Subcomponent.Builder
interface Builder {
SonComponent build();
}
}
@ManScope
@Component(modules = CarModule.class)
public interface ManComponent {
void injectMan(Man man);
SonComponent.Builder sonComponent(); // 用来创建 Subcomponent
}
手动注入
class Monkey {
@Inject
lateinit var stick: Stick
}
在上面这个例子中,Monkey 需要一个 Stick,但是 Monkey 并没有被 @Inject 注解的构造方法,也没有通过 Module 创建 Monkey 实例,所以 Dagger 框架无法构建 Monkey 实例,这时就需要使用到 Dagger 的手动注入能力
@Component
interface MonkeyComponent {
fun inject(monkey: Monkey)
}
val component = DaggerMonkeyComponent.create()
val monkey = Monkey()
component.inject(monkey) // 手动注入
val stick = monkey.stick
手动注入适用于 UI 入口类等无法手动创建的实例,但是也存在缺点:
Component 和 Monkey(UI 组件)需要在一起编译,这导致了服务提供方依赖了服务调用方,但在实际场景中,我们是不知道服务调用方的存在,这就引出了手动获取的方式
手动获取
@Component
interface MonkeyComponent {
fun stick(): Stick
}
val component = DaggerMonkeyComponent.create()
val stick = component.stick
- 手动注入可以获取到 Dagger 中管理的所有对象,但 Dagger 需要感知到被注入对象的存在
- 手动获取只能获取到 Dagger 主动暴露的对象,比较安全
- 手动注入适用于一些内部场景,既是提供方也是使用方。手动获取实例主要用于三方 SDK 对外提供能力,需要限制外部对内部的访问
外部注入
class Monkey {
@Inject
lateinit var stick: Stick
@Inject
lateinit var companyName: String
}
由于 Monkey 也需要工作,而具体的公司需要外部来指定,这时我们就需要外部注入的功能
@Component
interface MonkeyComponent {
fun stick(): Stick
@Component.Builder
interface Builder {
@BindInstance
fun companyName(name: String): Builder
fun build(): MonkeyComponent
}
}
MultiBinding
对应两个注解:IntoSet、IntoMap
Hilt
系统提供了默认的注入点 @AndroidEntryPoint,目前支持的有:Application、Activity、Fragment、View、Service、BroadcastReceiver
Hilt 是由 Google 推出的基于 Dagger 2 的依赖注入框架,旨在简化 Android 应用中的依赖注入流程。下面是 Hilt 和 Dagger 2 之间的一些主要区别和使用场景:
-
简化配置:
- Dagger 2 需要开发人员手动编写大量的代码来配置和连接依赖项。这包括编写模块、组件、限定符等。
- Hilt 提供了注解处理器,可以自动生成大部分 Dagger 2 中需要手动编写的代码,使得配置变得更加简单和直观。
-
减少模板代码:
- 在 Dagger 2 中,需要编写很多模板代码来连接各个依赖项和提供全局依赖项。
- Hilt 提供了注解和生成器,可以自动生成大部分模板代码,使得开发者只需专注于业务逻辑,而无需编写重复的模板代码。
-
注解简化:
- Dagger 2 的注解相对较复杂,需要开发人员对其有一定的了解才能正确地使用。
- Hilt 提供了一套更加简化的注解,使得开发者更容易理解和使用依赖注入功能。
-
Android 特化:
- Hilt 被设计成更适合在 Android 应用中使用,提供了特定于 Android 的功能,如与 Android 生命周期的集成、支持 ViewModel 的依赖注入等。
- Dagger 2 是一个通用的依赖注入框架,可以用于任何 Java 或 Android 应用,但是需要开发者自行处理 Android 特定的问题。
使用场景:
- 如果你是一个 Android 开发者,并且希望简化你的应用中的依赖注入流程,那么可以考虑使用 Hilt。
- 如果你的项目比较复杂,需要更多的灵活性和控制,或者已经熟悉了 Dagger 2,并且不想引入新的依赖库,那么可以继续使用 Dagger 2。
ARouter & Dagger2
如果你主要关注的是实现组件之间的页面跳转、参数传递以及拦截器等路由功能,那么 ARouter 可能更适合你的需求。ARouter 提供了简单易用的 API,能够方便地实现这些功能,并且在阿里巴巴内部得到了广泛的应用,有着较为成熟的技术支持和社区。
而如果你更关注的是依赖注入方面的功能,例如管理组件之间的依赖关系、提高代码的可测试性和可维护性等,那么 Dagger2 可能更适合你的需求。Dagger2 是一个强大的依赖注入框架,能够帮助你管理复杂的依赖关系,并且通过依赖注入的方式来实现解耦,提高代码质量。
在实际项目中,你也可以同时使用 ARouter 和 Dagger2,根据具体的需求选择合适的工具来解决问题。比如,你可以使用 ARouter 来管理页面跳转和路由功能,同时使用 Dagger2 来管理依赖关系,以此来充分发挥它们各自的优势。