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

  • 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。例如,您可以定义Engine的一个新子类,称为ElectricEngine,您希望Car使用它。如果您使用DI,您只需传入更新的ElectricEngine子类的实例,Car仍然可以工作,无需任何进一步的更改。

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

在Android中进行依赖注入主要有两种方法

  • **构造函数注入**。这是上面描述的方法。您将类的依赖项传递给它的构造函数。

  • **字段注入(或Setter注入)**。某些Android框架类(例如活动和片段)由系统实例化,因此构造函数注入是不可能的。使用字段注入,依赖项在类创建后实例化。代码如下所示

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

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

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

其他资源

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

示例