Android 中的依赖项注入

依赖项注入 (DI) 是一种在编程中广泛使用的技术,非常适合 Android 开发。遵循 DI 的原则,可以为良好的应用架构奠定基础。

实现依赖项注入可为您带来以下优势

  • 代码可重用性
  • 易于重构
  • 易于测试

依赖项注入的基础知识

在具体介绍 Android 中的依赖项注入之前,本页面将更一般地概述依赖项注入的工作原理。

什么是依赖项注入?

类通常需要引用其他类。例如,Car 类可能需要引用 Engine 类。这些所需的类称为依赖项,在本例中,Car 类依赖于 Engine 类的一个实例才能运行。

类获取所需对象有以下三种方式

  1. 类构造其所需的依赖项。在上述示例中,Car 会创建并初始化其自己的 Engine 实例。
  2. 从其他地方获取。某些 Android API(例如 Context getter 和 getSystemService())就是这样工作的。
  3. 作为参数提供。应用可以在类构造时提供这些依赖项,或将其传递给需要每个依赖项的函数。在上述示例中,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 class without dependency injection

这不是依赖项注入的示例,因为 Car 类正在构造其自己的 Engine。这可能会有问题,因为

  • CarEngine 紧密耦合 - Car 的一个实例使用一种类型的 Engine,无法轻松使用子类或替代实现。如果 Car 要构造其自己的 Engine,则您必须创建两种类型的 Car,而不是为 GasElectric 类型的引擎重用相同的 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();
    }
}
Car class using dependency injection

main 函数使用 Car。由于 Car 依赖于 Engine,因此应用会创建 Engine 的实例,然后使用它来构造 Car 的实例。这种基于 DI 的方法的优势在于

  • Car 的可重用性。您可以将 Engine 的不同实现传递给 Car。例如,您可能定义一个名为 ElectricEngine 的新 Engine 子类,并希望 Car 使用它。如果您使用 DI,您只需传入更新后的 ElectricEngine 子类的实例,Car 仍可正常工作,而无需进行任何进一步更改。

  • 轻松测试 Car。您可以传入测试替身来测试不同的场景。例如,您可以创建一个名为 FakeEngineEngine 测试替身,并为不同的测试配置它。

在 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 表面可验证的一部分,因此可以在对象创建时或编译时进行检查,而不是作为实现细节隐藏起来。

  • 易于测试:一个类不管理其依赖项,因此在测试它时,您可以传入不同的实现来测试所有不同的情况。

要完全理解依赖项注入的优势,您应该像手动依赖项注入中所示的那样,在您的应用中手动尝试。

其他资源

要了解有关依赖项注入的更多信息,请参阅以下其他资源。

示例