Android 中的依赖注入

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

实现依赖注入可以为您提供以下优势

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

依赖注入的基础知识

在专门介绍 Android 中的依赖注入之前,此页面将更全面地概述依赖注入的工作原理。

什么是依赖注入?

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

类获取所需对象有三种方法

  1. 类构造它所需的依赖项。在上面的示例中,Car 将创建并初始化它自己的 Engine 实例。
  2. 从其他地方获取。某些 Android API(例如 Context 获取器和 getSystemService())以这种方式工作。
  3. 将其作为参数提供。应用可以在构造类时提供这些依赖项,或将它们传递给需要每个依赖项的函数。在上面的示例中,Car 构造函数将接收 Engine 作为参数。

第三个选项是依赖注入!使用这种方法,您将获取类的依赖项并提供它们,而不是让类实例本身获取它们。

这是一个示例。在没有依赖注入的情况下,在代码中表示创建其自身 Engine 依赖项的 Car 如下所示

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。这可能存在问题,因为

  • 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();
    }
}
Car class using dependency injection

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

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

  • 轻松测试 “Car”。您可以传入测试替身来测试不同的场景。例如,您可以创建 “Engine” 的测试替身,称为 “FakeEngine”,并将其配置为用于不同的测试。

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

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

要充分了解依赖注入的优势,您应该像 手动依赖注入 中所示的那样,在您的应用程序中手动尝试它。

其他资源

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

示例