依赖项注入 (DI) 是一种在编程中广泛使用的技术,非常适合 Android 开发。遵循 DI 的原则,可以为良好的应用架构奠定基础。
实现依赖项注入可为您带来以下优势
- 代码可重用性
- 易于重构
- 易于测试
依赖项注入的基础知识
在具体介绍 Android 中的依赖项注入之前,本页面将更一般地概述依赖项注入的工作原理。
什么是依赖项注入?
类通常需要引用其他类。例如,Car
类可能需要引用 Engine
类。这些所需的类称为依赖项,在本例中,Car
类依赖于 Engine
类的一个实例才能运行。
类获取所需对象有以下三种方式
- 类构造其所需的依赖项。在上述示例中,
Car
会创建并初始化其自己的Engine
实例。 - 从其他地方获取。某些 Android API(例如
Context
getter 和getSystemService()
)就是这样工作的。 - 作为参数提供。应用可以在类构造时提供这些依赖项,或将其传递给需要每个依赖项的函数。在上述示例中,
Car
构造函数将接收Engine
作为参数。
第三种方式就是依赖项注入!通过这种方法,您获取类的依赖项并提供它们,而不是让类实例自行获取。
示例如下。不使用依赖项注入,表示 Car
在代码中创建其自己的 Engine
依赖项的代码如下所示
Kotlin
class Car { private val engine = Engine() fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.start() }
Java
class Car { private Engine engine = new Engine(); public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.start(); } }

这不是依赖项注入的示例,因为 Car
类正在构造其自己的 Engine
。这可能会有问题,因为
Car
和Engine
紧密耦合 -Car
的一个实例使用一种类型的Engine
,无法轻松使用子类或替代实现。如果Car
要构造其自己的Engine
,则您必须创建两种类型的Car
,而不是为Gas
和Electric
类型的引擎重用相同的Car
。对
Engine
的硬依赖使得测试更加困难。Car
使用真实的Engine
实例,因此无法使用测试替身来修改Engine
以适应不同的测试用例。
使用依赖项注入的代码是什么样的?Car
的每个实例不再在初始化时构造自己的 Engine
对象,而是在其构造函数中将 Engine
对象作为参数接收
Kotlin
class Car(private val engine: Engine) { fun start() { engine.start() } } fun main(args: Array) { val engine = Engine() val car = Car(engine) car.start() }
Java
class Car { private final Engine engine; public Car(Engine engine) { this.engine = engine; } public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Engine engine = new Engine(); Car car = new Car(engine); car.start(); } }

main
函数使用 Car
。由于 Car
依赖于 Engine
,因此应用会创建 Engine
的实例,然后使用它来构造 Car
的实例。这种基于 DI 的方法的优势在于
Car
的可重用性。您可以将Engine
的不同实现传递给Car
。例如,您可能定义一个名为ElectricEngine
的新Engine
子类,并希望Car
使用它。如果您使用 DI,您只需传入更新后的ElectricEngine
子类的实例,Car
仍可正常工作,而无需进行任何进一步更改。轻松测试
Car
。您可以传入测试替身来测试不同的场景。例如,您可以创建一个名为FakeEngine
的Engine
测试替身,并为不同的测试配置它。
在 Android 中进行依赖项注入主要有两种方式
构造函数注入。这是上述方法。您将类的依赖项传递给其构造函数。
字段注入(或 Setter 注入)。某些 Android 框架类(例如 activity 和 fragment)由系统实例化,因此无法进行构造函数注入。通过字段注入,依赖项在类创建后实例化。代码如下所示
Kotlin
class Car { lateinit var engine: Engine fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.engine = Engine() car.start() }
Java
class Car { private Engine engine; public void setEngine(Engine engine) { this.engine = engine; } public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.setEngine(new Engine()); car.start(); } }
自动化依赖项注入
在前面的示例中,您亲自创建、提供和管理不同类的依赖项,而没有依赖库。这称为手动依赖项注入。在 Car
示例中,只有一个依赖项,但更多的依赖项和类会使手动注入依赖项变得更加繁琐。手动依赖项注入还存在以下几个问题
对于大型应用,获取所有依赖项并正确连接它们可能需要大量的样板代码。在多层架构中,为了为顶层创建对象,您必须提供其下方所有层的依赖项。举一个具体的例子,要制造一辆真正的汽车,您可能需要发动机、变速箱、底盘和其他部件;而发动机又需要气缸和火花塞。
当您无法在传入依赖项之前构造它们时(例如,在使用惰性初始化或将对象作用域限定到应用流时),您需要编写和维护一个自定义容器(或依赖项图),用于管理内存中依赖项的生命周期。
有一些库通过自动化创建和提供依赖项的过程来解决这个问题。它们分为两类
基于反射的解决方案,在运行时连接依赖项。
生成代码以在编译时连接依赖项的静态解决方案。
Dagger 是一个流行的 Java、Kotlin 和 Android 依赖项注入库,由 Google 维护。Dagger 通过为您创建和管理依赖项图来促进在应用中使用 DI。它提供完全静态和编译时依赖项,解决了 Guice 等基于反射的解决方案的许多开发和性能问题。
依赖项注入的替代方案
依赖项注入的替代方案是使用服务定位器。服务定位器设计模式也改进了类与具体依赖项的解耦。您会创建一个名为服务定位器的类,该类创建并存储依赖项,然后按需提供这些依赖项。
Kotlin
object ServiceLocator { fun getEngine(): Engine = Engine() } class Car { private val engine = ServiceLocator.getEngine() fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.start() }
Java
class ServiceLocator { private static ServiceLocator instance = null; private ServiceLocator() {} public static ServiceLocator getInstance() { if (instance == null) { synchronized(ServiceLocator.class) { instance = new ServiceLocator(); } } return instance; } public Engine getEngine() { return new Engine(); } } class Car { private Engine engine = ServiceLocator.getInstance().getEngine(); public void start() { engine.start(); } } class MyApp { public static void main(String[] args) { Car car = new Car(); car.start(); } }
服务定位器模式与依赖项注入的不同之处在于元素的消耗方式。使用服务定位器模式时,类拥有控制权并请求注入对象;而使用依赖项注入时,应用拥有控制权并主动注入所需对象。
与依赖项注入相比
服务定位器所需的依赖项集合使代码更难测试,因为所有测试都必须与相同的全局服务定位器交互。
依赖项编码在类实现中,而不是在 API 表面中。因此,很难从外部了解一个类需要什么。因此,对
Car
或服务定位器中可用依赖项的更改可能会导致引用失败,从而导致运行时或测试失败。如果您想将对象作用域限定为应用整个生命周期之外的任何内容,则管理对象的生命周期会更加困难。
在您的 Android 应用中使用 Hilt
Hilt 是 Jetpack 推荐的 Android 依赖项注入库。Hilt 通过为项目中的每个 Android 类提供容器并自动管理它们的生命周期,定义了在应用中执行 DI 的标准方式。
Hilt 构建在流行的 DI 库 Dagger 之上,以受益于 Dagger 提供的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。
要了解有关 Hilt 的更多信息,请参阅使用 Hilt 进行依赖项注入。
总结
依赖项注入为您的应用提供了以下优势
类的可重用性和依赖项的解耦:更容易替换依赖项的实现。由于控制反转,代码重用性得到提高,并且类不再控制其依赖项的创建方式,而是与任何配置一起工作。
易于重构:依赖项成为 API 表面可验证的一部分,因此可以在对象创建时或编译时进行检查,而不是作为实现细节隐藏起来。
易于测试:一个类不管理其依赖项,因此在测试它时,您可以传入不同的实现来测试所有不同的情况。
要完全理解依赖项注入的优势,您应该像手动依赖项注入中所示的那样,在您的应用中手动尝试。
其他资源
要了解有关依赖项注入的更多信息,请参阅以下其他资源。