【Android进阶】Android平台主流依赖注入方案对比

本文介绍了Android 平台的主流依赖注入方案对比,主要涉及Dagger,Hilt,Koin三种
当初学者熟悉软件在该平台上的运行机制,开始大量写代码之后,在软件项目的架构设计上应该符合 SOLID 原则。
SOLID 是 Robert C. Martin(“Uncle Bob”) 提出的一组五个基本原则的首字母缩写,旨在帮助开发者设计更易于理解、维护和扩展的软件系统:
Single Responsibility Principle (单一职责原则)
Open/Closed Principle (开闭原则)
Liskov Substitution Principle (里氏替换原则)
Interface Segregation Principle (接口隔离原则)
Dependency Inversion Principle (依赖反转原则)
其中的依赖反转原则是指:
高层模块不应该依赖低层模块,两者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。
简单来说,就是我们在设计系统时,不应该让高层组件直接依赖于低层组件的具体实现,而是应该让它们都依赖于抽象(例如接口或抽象类)。
什么是依赖?
在软件开发中,“依赖”指的是一个对象需要另一个对象来完成其功能。比如,一个 Car 对象可能需要一个 Engine 对象才能启动和运行。这时,我们可以说 Car 依赖 Engine。
传统的做法是,Car 对象在自己的内部创建或查找 Engine 对象:
class Car {
private Engine engine;
public Car() {
this.engine = new Engine(); // Car 自己创建了 Engine 对象
}
public void start() {
engine.ignite();
System.out.println("Car started!");
}
}
这种也叫直接依赖,其问题在于:
- 紧耦合: 如果低层模块的实现细节发生变化,高层模块也可能需要修改。
- 测试困难: 在测试高层模块时,你不得不依赖真实的低层模块,这使得单元测试变得复杂且效率低下。你无法轻易地替换一个模拟的数据访问层。
- 可扩展性差: 增加新的低层实现会影响到所有依赖它的高层模块。
什么是依赖注入?
依赖注入的核心思想是:一个对象不应该自己创建或查找它所依赖的对象,而是应该由外部(通常是一个“注入器”或“容器”)提供这些依赖。 依赖反转是我们的设计目标,依赖注入就是实现的路径。
在 Car 和 Engine 的例子中,如果使用依赖注入,Car 就不再负责创建 Engine,而是等待外部把 Engine “注入”进来。结合Kotlin的构造函数写法, Car 类可以写成下面这种简洁的形式:
class Car(val engine: Engine) {
public void start() {
engine.ignite();
System.out.println("Car started!");
}
}
这种设计方案有哪些好处呢?
首先最明显的就是 解耦 ,对象不再需要关心它所依赖对象的创建细节,它们只需要知道如何使用这些依赖。这使得代码更灵活,更容易修改和扩展。
其次是 可测试性 和 可维护性 提高了。更有利于单元测试,同时当外部的依赖发生变化时,只需要修改创建和提供依赖的部分,而不需要修改所有依赖该对象的代码。
还可以 提高代码复用性 ,独立的对象可以更容易地在不同的场景和组件中重用。
在 Android 端的依赖注入设计理念,整体的发展方向是从最初的手动管理到功能强大的自动化框架。
在早期的 Android 开发中,并没有成熟的 DI 框架。通常采用直接创建依赖示例的方式,后面又出现了由策略模式驱动的服务定位器的形式来提供依赖。
- 手动实例化 这是最直接的方式,在一个类中直接
new出它所需要的依赖。多了之后导致代码紧耦合,难以测试和维护,特别是当依赖链很深时,修改一个地方可能需要改动很多地方。 - 服务定位器 这个模式会引入一个中央注册表(或单例)来存储和提供依赖。类需要依赖时,就向这个注册表“请求”对应的实例。
- 优点: 相对于手动实例化,服务定位器提供了一定程度的解耦,因为消费者不再直接创建依赖。
- 问题: 仍然存在隐藏依赖。你不知道一个类需要哪些依赖,除非查看其实现。难以追踪对象生命周期,并且测试时替换模拟对象不够优雅。它更像是“查找依赖”而不是“注入依赖”。
Dagger 1:初次尝试编译时注入
在 2012 年左右,Square 公司推出了 Dagger。这是 Android 平台第一个真正意义上的依赖注入框架,并且它采用了编译时代码生成的方式。
没有运行时的反射开销,带来了性能优势。它使用了 注解处理器 在编译阶段生成注入代码。
但是 Dagger 的配置和使用相对复杂,尤其是对于大型项目而言,编写和维护大量的模块 (Module) 和组件 (Component) 样板代码 成为一个挑战。
Dagger 2:性能与可扩展性的飞跃
2015 年,Google 接手 Dagger 项目并发布了全新的 Dagger 2。Dagger 2 是对 Dagger 1 的彻底重写,它秉承了 Dagger 1 的 编译时生成 的特性,但在设计理念和实现上有了重大改进。
Dagger 2 在编译时生成代码,这些代码负责实例化对象并提供它们的依赖。这意味着在运行时没有反射开销,性能非常高。它通过注解处理器来分析你的代码,生成一个 依赖图 ,然后根据这个图生成相应的 Java 代码。它生成的是直接的 Java 代码,模拟了你在手写工厂类和提供器时的行为,从而在性能上达到了极致。
运行 Dagger 2 示例需要添加 Dagger 依赖并配置注解处理器。在 Android 项目中,通常在 build.gradle 文件中配置。比如使用Kotlin的话,需要在 build.gradle 中配置 kapt 插件:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt' // 配置 Kotlin 注解处理器插件
}
Dagger 2 在使用时需要定义模块和组件。
- 定义依赖接口和实现:
// repository/UserRepository.java interface UserRepository { void saveUser(String username); } // repository/DatabaseUserRepository.java class DatabaseUserRepository implements UserRepository { @Override public void saveUser(String username) { System.out.println("Saving user " + username + " to database."); } } - 定义提供依赖的 Module:
import dagger.Module; import dagger.Provides; import javax.inject.Singleton; // Dagger 2 提供的作用域注解 // di/AppModule.java @Module public class AppModule { @Provides // 提供 UserRepository 实例 @Singleton // 将 UserRepository 定义为单例 UserRepository provideUserRepository() { return new DatabaseUserRepository(); } } - 定义注入器 Component:
import dagger.Component; import javax.inject.Singleton; // di/AppComponent.java @Singleton // AppComponent 也是单例作用域 @Component(modules = AppModule.class) // 关联 AppModule public interface AppComponent { // 定义注入方法,MyPresenter 可以通过这个方法被注入依赖 void inject(MyPresenterWithDagger presenter); } - 在需要注入的类中使用
@Inject:import javax.inject.Inject; // presenter/MyPresenterWithDagger.java class MyPresenterWithDagger { @Inject // 声明需要注入 UserRepository UserRepository userRepository; public MyPresenterWithDagger() { // Dagger 2 会在调用 inject(this) 后自动填充 userRepository } public void registerUser(String username) { userRepository.saveUser(username); System.out.println("User " + username + " registered."); } } - 在 Application 或 Main 方法中初始化和使用:
// main/MainDagger2.java public class MainDagger2 { public static void main(String[] args) { // ✨ 构建 Dagger 组件,这个 DaggerAppComponent 是 Dagger 2 编译时生成的 AppComponent component = DaggerAppComponent.builder().build(); MyPresenterWithDagger presenter = new MyPresenterWithDagger(); // ✨ 执行注入操作,Dagger 会找到 @Inject 标注的字段并填充依赖 component.inject(presenter); presenter.registerUser("Alice"); } }
Dagger 2 具有如下优点:
- 极高性能: 纯粹的编译时生成代码,运行时无反射开销,性能极佳。
- 类型安全: 编译时即可发现依赖错误,将运行时崩溃降到最低。
- 强大的模块化能力: 提供了
@Module、@Provides、@Component、@Subcomponent、@Scope等丰富的注解,可以精细地控制依赖的提供和生命周期。
Dagger 2 推出之后,很快成为 Android 平台最主流、最强大的 DI 框架。
它解决了大规模项目中的依赖管理难题,但也继承了其复杂性,仍然需要开发者投入大量时间学习和配置。
Hilt:Google 官方简化 Dagger
2020 年,Google 推出了 Hilt,这是构建在 Dagger 2 之上的 Android 官方推荐的依赖注入库。Hilt 的主要目标是简化 Dagger 在 Android 应用中的使用。大量减少项目中为了实现依赖注入而创建的重复的样板代码。
值得一提的是,在Google官方开源的旨在展示最新Android技术的开源项目—— NowInAndroid 中,就使用了Hilt来实现依赖注入。
Hilt 的核心思想是通过提供一套 标准化的 Android 组件绑定 (例如 @AndroidEntryPoint、@ApplicationContext、@ActivityContext 等),以及预定义的作用域,极大地 减少了 Dagger 所需的样板代码 和手动配置。
Hilt 基于注解实现,针对每个需要被注入的属性,Hilt 都会基于 KAPT/KSP 在编译期间查找它的注入源头,并生成一对一的注入方法。
添加依赖和插件:
// project/build.gradle buildscript { dependencies { classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1' // 检查最新版本 } } // app/build.gradle plugins { id 'kotlin-kapt' // 或 id 'androidx.navigation.safeargs.kotlin' 如果使用 kotlin id 'com.google.dagger.hilt.android' } dependencies { implementation 'com.google.dagger:hilt-android:2.51.1' kapt 'com.google.dagger:hilt-compiler:2.51.1' // ... 其他依赖 }在 Application 类上添加
@HiltAndroidApp:import android.app.Application import dagger.hilt.android.HiltAndroidApp // di/MyApplication.kt @HiltAndroidApp // Hilt 的入口点,触发代码生成 class MyApplication : Application() { // 通常不需要在这里写额外的代码,Hilt 会自动管理组件 }定义依赖接口和实现 (与 Dagger 类似):
// repository/UserRepository.kt interface UserRepository { fun saveUser(username: String) } // repository/DatabaseUserRepository.kt // Hilt 可以在构造函数上直接使用 @Inject 来告知如何创建实例 import javax.inject.Inject import javax.inject.Singleton @Singleton // Hilt 也支持 Dagger 的作用域注解 class DatabaseUserRepository @Inject constructor() : UserRepository { // 🚀 构造函数注入的标志 override fun saveUser(username: String) { println("Saving user $username to database.") } }注意: 如果
DatabaseUserRepository的构造函数没有参数,或者其参数都可以被 Hilt 自动提供,那么可以直接使用@Inject constructor()。对于第三方库或接口,仍然需要使用@Module和@Provides。定义 Hilt 模块(针对接口或外部类):
import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton // di/AppModule.kt @Module @InstallIn(SingletonComponent::class) // 🚀 指定模块安装到哪个 Hilt 组件(例如 Application 级别) abstract class AppModule { // 使用 abstract class 可以更高效地绑定接口 @Binds // 🚀 绑定接口到具体实现 @Singleton abstract fun bindUserRepository(impl: DatabaseUserRepository): UserRepository }在 Android 组件上使用
@AndroidEntryPoint和@Inject:import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject // activity/MainActivity.kt @AndroidEntryPoint // 🚀 标记这是一个 Hilt 入口点,Hilt 会为它生成组件并注入依赖 class MainActivity : AppCompatActivity() { @Inject // 🚀 自动注入 UserRepository 实例 lateinit var userRepository: UserRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) userRepository.saveUser("Charlie") println("User Charlie saved from MainActivity.") } }
Hilt具有如下优点:
- 集成度高: 与 Android 框架组件(Activity, Fragment, ViewModel, Service 等)无缝集成,自动生成 Dagger 组件。
- 易用性: 显著降低了 Dagger 的学习曲线和使用门槛。
- Google 官方支持: 作为官方推荐的 DI 解决方案,Hilt 在未来的发展和维护上更有保障。
- 保留 Dagger 优势: 依然是编译时注入,拥有 Dagger 的高性能和类型安全。
Hilt 迅速成为 Android DI 的“新宠”,尤其适合新项目和希望简化 Dagger 配置的现有项目。在 nowinandroid 中使用,也代表了它是当前官方认为的 Android 依赖注入的最佳实践。
Koin:运行时注入的轻量级选择
随着 Kotlin 在 Android 领域的崛起,又出现了一些纯 Kotlin 编写的 DI 框架。其中,Koin 在 2017 年左右脱颖而出。
Koin 采取了与 Dagger 完全不同的策略,它是一个运行时依赖注入框架,不使用注解处理器,而是利用 Kotlin 的 DSL(领域特定语言)来声明依赖。Koin 在运行时通过 Kotlin 的 DSL 解析依赖关系,不依赖反射或注解处理器,避免了 Dagger 的编译时代码生成复杂性,启动速度更快。
Koin 除了使用上轻量化,还具有以下优点:它的配置简单,学习曲线平缓,几乎没有样板代码。与 Kotlin 语言特性无缝集成,代码简洁。由于没有注解处理,编译速度通常比 Dagger 更快。
同时,Koin 由于运行时生成的特点,如果有些依赖配置出错,只有在运行时才可以发现。
Koin 采用 Kotlin DSL,不需要注解处理器。
步骤:
- 定义依赖接口和实现 (与 Dagger 类似):
// repository/UserRepository.kt interface UserRepository { fun saveUser(username: String) } // repository/DatabaseUserRepository.kt class DatabaseUserRepository : UserRepository { override fun saveUser(username: String) { println("Saving user $username to database.") } } - 定义 Koin 模块:
import org.koin.dsl.module // di/appModule.kt val appModule = module { // single 表示单例,get() 会自动解析并提供所需的依赖 single<UserRepository> { DatabaseUserRepository() } } - 定义需要依赖的类:
import org.koin.core.component.KoinComponent import org.koin.core.component.inject // presenter/MyPresenterWithKoin.kt class MyPresenterWithKoin : KoinComponent { // 实现 KoinComponent 接口 // 🚀 通过 inject() 委托属性来获取依赖 private val userRepository: UserRepository by inject() fun registerUser(username: String) { userRepository.saveUser(username) println("User $username registered.") } } - 在 Application 或 Main 方法中启动 Koin:
import org.koin.core.context.startKoin import org.koin.core.context.stopKoin // main/MainKoin.kt fun main() { // ✨ 启动 Koin 上下文,并加载模块 startKoin { modules(appModule) } val presenter = MyPresenterWithKoin() presenter.registerUser("Bob") stopKoin() // 清理 Koin 上下文 }
Koin 的核心优势在于 简洁性 和 Kotlin 原生支持,通过 DSL 和运行时解析降低了 DI 的学习成本,适合追求开发效率的项目。但对于超大型应用或对性能极度敏感的场景,可能需要权衡其运行时解析的开销。如果你正在使用 Kotlin 开发 Android 或后端服务,Koin 是一个值得尝试的轻量级 DI 方案。
现如今,Kotlin Multiplatform 跨平台的迅速发展,Koin 也推出了其跨平台版本,在 Android,IOS,Desktop和 web 端 的Kotlin跨平台项目里,都可以助力开发者实现依赖注入,支持功能的快速开发。