Skip to content

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 主流框架

框架类型编译时学习曲线推荐度
HiltAndroid 官方⭐⭐⭐⭐⭐
Dagger 2Google⭐⭐⭐⭐
KoinKotlin 原生⭐⭐⭐⭐
KodeinKotlin 原生⭐⭐⭐
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 优点 ✅

  1. 解耦 - 类之间依赖关系清晰
  2. 可测试 - 易于 Mock 和单元测试
  3. 可维护 - 代码结构清晰
  4. 可复用 - 依赖可以共享
  5. 灵活 - 易于替换实现

6.2 缺点 ❌

  1. 学习成本 - 概念复杂,需要时间学习
  2. 代码量 - 需要编写额外的注解和配置
  3. 调试困难 - 编译时框架调试复杂
  4. 性能开销 - 运行时框架有性能损耗
  5. 反射问题 - 运行时框架使用反射

七、面试核心考点

7.1 基础问题

Q1: 什么是依赖注入?

A:

  • 一种设计模式,将对象的创建和依赖的提供交给外部
  • 实现松耦合、高内聚
  • 核心是"依赖倒置原则"

Q2: DI 的三种注入方式?

A:

  1. 构造函数注入 - 推荐,依赖明确
  2. 字段注入 - 简洁但不安全
  3. 方法注入 - 适合可选依赖

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 核心要点

  1. DI 是松耦合的关键 - 提升可测试性和可维护性
  2. 构造函数注入最安全 - 依赖明确、不可变
  3. 依赖抽象而非实现 - 遵循开闭原则
  4. Hilt 是 Android 首选 - 官方支持、编译时检查
  5. 学习曲线值得投入 - 长期收益巨大

10.2 学习路径

1. 理解 SOLID 原则
2. 掌握构造函数注入
3. 学习 Hilt 基础
4. 理解作用域管理
5. 掌握高级特性(Qualifier、Multibinding)
6. 对比其他框架(Koin、Dagger)

📚 参考资料

🔗 下一篇: [Hilt 框架详解](02_Hilt 框架.md)