Appearance
DI 依赖注入基础概念 📚
Android 开发中最重要的设计模式之一,大厂面试必考
一、什么是依赖注入?
1.1 核心概念
依赖注入(Dependency Injection, DI)是一种设计模式
传统方式 vs 依赖注入
❌ 传统方式(紧耦合)
class NetworkManager {
private val repository = UserRepository() // 直接创建,紧耦合
}
✅ 依赖注入(松耦合)
class NetworkManager {
private val repository: UserRepository
constructor(repository: UserRepository) {
this.repository = repository // 依赖被注入
}
}1.2 核心术语
| 术语 | 英文 | 解释 |
|---|---|---|
| 依赖 | Dependency | 一个类需要使用的其他类 |
| 注入 | Injection | 将依赖对象传入,而非自己创建 |
| 倒置 | Inversion | 依赖方向反转(DI = IoC) |
| 容器 | Container | 管理对象创建和注入的框架 |
| 模块 | Module | 提供依赖的定义 |
| 作用域 | Scope | 对象的生命周期管理 |
二、依赖注入的三种方式
2.1 构造函数注入(Constructor Injection)⭐⭐⭐⭐⭐
kotlin
// ✅ 推荐方式
class UserRepository @Inject constructor(
private val apiService: ApiService,
private val dao: UserDao
) {
// 依赖在构造函数中注入
}
// 使用
val repository = UserRepository(apiService, dao)优点:
- 依赖明确(看构造函数就知道需要什么)
- 不可变(使用 val)
- 易于测试
- 避免 null 检查
缺点:
- 依赖过多时构造函数过长
2.2 字段注入(Field Injection)⭐
kotlin
// ❌ 不推荐,但在某些框架中使用
class MyActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: MyViewModel
@Inject
lateinit var repository: UserRepository
}优点:
- 代码简洁
- 框架自动注入
缺点:
- 无法保证初始化完成前使用
- 不利于单元测试
- 隐藏了依赖关系
2.3 方法注入(Method Injection)⭐⭐
kotlin
class MyActivity : AppCompatActivity() {
private lateinit var repository: UserRepository
@Inject
fun setRepository(repository: UserRepository) {
this.repository = repository
}
}优点:
- 可以在运行时多次注入
- 适合可选依赖
缺点:
- 需要手动管理生命周期
三、依赖倒置原则(DIP)
3.1 原则定义
依赖倒置原则(Dependency Inversion Principle)
- 高层模块不应依赖低层模块,两者都应依赖其抽象
- 抽象不应依赖细节,细节应依赖抽象3.2 代码示例
kotlin
// ❌ 违反 DIP - 依赖具体实现
class UserRepository {
private val apiService = ApiService() // 直接依赖具体类
}
// ✅ 遵循 DIP - 依赖抽象接口
interface UserRepositoryProvider {
fun getUser(id: Int): User
}
class UserRepositoryImpl @Inject constructor(
private val userProvider: UserRepositoryProvider
) {
fun getUser(id: Int) = userProvider.getUser(id)
}
class ApiServiceImpl : UserRepositoryProvider {
override fun getUser(id: Int): User {
// API 实现
}
}3.3 五大 SOLID 原则
S - Single Responsibility(单一职责)
O - Open/Closed(开闭原则)
L - Liskov Substitution(里氏替换)
I - Interface Segregation(接口隔离)
D - Dependency Inversion(依赖倒置)⭐四、为什么需要依赖注入?
4.1 解决的问题
问题 1:紧耦合
kotlin
// ❌ 紧耦合 - 难以替换和测试
class UserService {
private val userApi = RealUserApi() // 硬编码
private val userDao = SqliteUserDao() // 硬编码
fun getUser(id: Int): User {
return userApi.getUser(id)
}
}
// ✅ 松耦合 - 易于替换和测试
class UserService {
private val api: UserApi
private val dao: UserDao
constructor(api: UserApi, dao: UserDao) {
this.api = api
this.dao = dao
}
fun getUser(id: Int): User {
return api.getUser(id)
}
}问题 2:难以测试
kotlin
// ❌ 难以测试
class RealUserService {
private val api = RealUserApi()
fun getUser(id: Int): User {
return api.getUser(id) // 每次测试都要请求真实 API
}
}
// ✅ 易于测试
class UserService @Inject constructor(
private val api: UserApi // 可以传入 Mock
) {
fun getUser(id: Int): User {
return api.getUser(id)
}
}
// 单元测试
@Test
fun testGetUser() {
val mockApi = MockUserApi() // 传入 Mock
val service = UserService(mockApi)
val user = service.getUser(1)
// 无需网络,快速测试
}问题 3:对象创建复杂
kotlin
// ❌ 手动创建对象,层级深
class MainActivity {
private val repository = UserRepository(
RealUserApi(
Retrofit.Builder()
.baseUrl("https://api.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(UserApi::class.java)
),
AppDatabase.getDatabase(this).userDao()
)
}
// ✅ DI 框架自动创建
class MainActivity @HiltAndroidApp constructor(
@Inject val repository: UserRepository
)五、依赖注入框架对比
5.1 主流框架
| 框架 | 类型 | 编译时 | 学习曲线 | 推荐度 |
|---|---|---|---|---|
| Hilt | Android 官方 | ✅ | 低 | ⭐⭐⭐⭐⭐ |
| Dagger 2 | ✅ | 高 | ⭐⭐⭐⭐ | |
| Koin | Kotlin 原生 | ❌ | 低 | ⭐⭐⭐⭐ |
| Kodein | Kotlin 原生 | ❌ | 低 | ⭐⭐⭐ |
| ButterKnife | 绑定 | ✅ | 低 | ⭐(已废弃) |
5.2 Hilt vs Koin
kotlin
// ==== Hilt (编译时检查) ====
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideApi(): ApiService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.build()
.create(ApiService::class.java)
}
}
@HiltAndroidApp
class MyApplication : Application()
@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
@Inject lateinit var api: ApiService // ✅ 编译时检查
}
// ==== Koin (运行时检查) ====
val appModule = module {
single {
Retrofit.Builder()
.baseUrl(BASE_URL)
.build()
.create(ApiService::class.java)
}
}
class MyActivity : AppCompatActivity() {
val api: ApiService by inject() // ❌ 运行时才能发现错误
}六、依赖注入的优缺点
6.1 优点 ✅
- 解耦 - 类之间依赖关系清晰
- 可测试 - 易于 Mock 和单元测试
- 可维护 - 代码结构清晰
- 可复用 - 依赖可以共享
- 灵活 - 易于替换实现
6.2 缺点 ❌
- 学习成本 - 概念复杂,需要时间学习
- 代码量 - 需要编写额外的注解和配置
- 调试困难 - 编译时框架调试复杂
- 性能开销 - 运行时框架有性能损耗
- 反射问题 - 运行时框架使用反射
七、面试核心考点
7.1 基础问题
Q1: 什么是依赖注入?
A:
- 一种设计模式,将对象的创建和依赖的提供交给外部
- 实现松耦合、高内聚
- 核心是"依赖倒置原则"
Q2: DI 的三种注入方式?
A:
- 构造函数注入 - 推荐,依赖明确
- 字段注入 - 简洁但不安全
- 方法注入 - 适合可选依赖
Q3: Hilt 和 Dagger 的关系?
A:
- Hilt 基于 Dagger 2 构建
- Hilt 简化了 Dagger 的复杂性
- Hilt 专为 Android 优化
7.2 进阶问题
Q4: 为什么推荐构造函数注入?
A:
- 依赖关系明确
- 保证对象完全初始化
- 不可变(使用 val)
- 易于测试和 Mock
Q5: 依赖注入如何提升可测试性?
A:
kotlin
// 可以传入 Mock
class UserService @Inject constructor(
private val api: UserApi
)
@Test
fun test() {
val mockApi = MockUserApi() // 注入 Mock
val service = UserService(mockApi)
// 测试无需网络
}Q6: 编译时 DI 和运行时 DI 的区别?
A:
| 特性 | 编译时(Hilt/Dagger) | 运行时(Koin) |
|---|---|---|
| 检查时机 | 编译时 | 运行时 |
| 性能 | 高(无反射) | 低(使用反射) |
| 代码生成 | 生成代码 | 不生成代码 |
| 调试 | 复杂 | 简单 |
| 学习曲线 | 陡峭 | 平缓 |
八、实战最佳实践
8.1 推荐实践
kotlin
// ✅ 1. 使用构造函数注入
class Repository @Inject constructor(
private val api: ApiService
)
// ✅ 2. 依赖抽象而非具体实现
interface UserRepository {
fun getUser(id: Int): User
}
class RealUserRepository @Inject constructor(
api: ApiService
) : UserRepository
// ✅ 3. 使用不可变依赖
class ViewModel @Inject constructor(
private val repository: UserRepository // val
)
// ✅ 4. 接口作为注入点
class Activity @Inject constructor(
private val userRepository: UserRepository // 接口
)
// ✅ 5. 避免循环依赖
// 使用 lazy 或回调解决8.2 避免的陷阱
kotlin
// ❌ 1. 循环依赖
class A @Inject constructor(private val b: B)
class B @Inject constructor(private val a: A) // 循环!
// ❌ 2. 注入具体实现而非接口
class ViewModel @Inject constructor(
private val realApi: RealApi // 应该用接口
)
// ❌ 3. 在 Activity 中创建新实例
class Activity {
private val repository = UserRepository(api) // 应该注入
}
// ❌ 4. 注入可变依赖
class ViewModel @Inject constructor(
var repository: UserRepository // 应该用 val
)九、从手动 DI 到框架
9.1 手动依赖注入
kotlin
// 手动创建依赖图
class Application : Application() {
private val api = createApi()
private val dao = createDao()
private val repository = UserRepository(api, dao)
private val viewModel = ViewModel(repository)
fun provideRepository(): UserRepository = repository
fun provideViewModel(): ViewModel = viewModel
}
// Activity 中获取
class MainActivity : AppCompatActivity() {
private val repository = (application as Application).provideRepository()
}9.2 使用 Hilt
kotlin
// 自动创建依赖图
@HiltAndroidApp
class Application : Application()
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideApi(): ApiService = // ...
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repository: UserRepository // 自动注入
}十、总结
10.1 核心要点
- DI 是松耦合的关键 - 提升可测试性和可维护性
- 构造函数注入最安全 - 依赖明确、不可变
- 依赖抽象而非实现 - 遵循开闭原则
- Hilt 是 Android 首选 - 官方支持、编译时检查
- 学习曲线值得投入 - 长期收益巨大
10.2 学习路径
1. 理解 SOLID 原则
2. 掌握构造函数注入
3. 学习 Hilt 基础
4. 理解作用域管理
5. 掌握高级特性(Qualifier、Multibinding)
6. 对比其他框架(Koin、Dagger)📚 参考资料
🔗 下一篇: [Hilt 框架详解](02_Hilt 框架.md)