Skip to content

依赖注入 以及 控制反转

这也是解耦合的一种方式,

核心概念

  • DIP(依赖反转原则):高层模块不应该依赖低层模块,二者都应依赖抽象(接口/抽象类);抽象不应依赖细节,细节应依赖抽象。
  • IoC(控制反转):不由类自己去new出其依赖,而是由外部容器或代码负责提供依赖(反转控制权)。
  • 依赖注入(DI):实现 IoC 的一种方式 —— 把依赖“注入”给消费者(构造器注入、属性/字段注入、方法注入)。

为什么要用(优点)

  • 更易测试(可替换 mock/假实现)
  • 降耦合、提高可维护性、易扩展
  • 更清晰地依赖边界与生命周期管理(配合 DI 框架更强大)

alt_text

常见实现方式与示例

1) 手动依赖注入(最简单、适合小项目 / 测试)

思想:通过构造函数把依赖传入(Constructor Injection)。简单明了,利于单元测试。

kotlin
// 接口与实现
interface UserRepository {
    fun getUser(id: String): User
}

class UserRepositoryImpl(private val api: UserApi) : UserRepository {
    override fun getUser(id: String) = api.fetch(id)
}

// 使用方(ViewModel)
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
    fun load(id: String) { /* use userRepository */ }
}

// 在 Activity/Fragment/Application 中组装依赖
class MyApplication : Application() {
    // 简单工厂/单例
    val userApi by lazy { UserApi() }
    val userRepository by lazy { UserRepositoryImpl(userApi) }
}

class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: UserViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val repo = (application as MyApplication).userRepository
        viewModel = UserViewModel(repo) // 注入
    }
}
  • 优点:简单、可控、易理解。

  • 缺点:在复杂对象图或需要作用域管理时会变臃肿(许多by lazy/工厂代码)。

    • Google的例子就说得很清晰了,Google举了一个例子,大概意思就是说,你要new一辆车的时候,你需要把汽车的引擎、轮胎等等内容,都要放进去。

    • 而汽车的引擎,很可能又依赖于其他物件的形成,那么这种new到底要new到什么时候呢?

    • 回到Android的例子,以MVVM来说,ViewModel由Repository组成,而Repository又由UseCase组成。

      kotlin
      class HomefeedViewModel(
          private val repo: HomefeedRepo
      ) {
          // ...
      }
      
      class HomefeedRepo(
          private val repo: HomefeedRepo
      ) {
          // ...
      }
      
      class HomefeedUseCase(
          private val service: HomefeedService
      ) {
          // ...
      }
      
      class HomefeedService {
          // ...
      }
      
      class HomefeedViewModelFactory(
          private val homefeedRepo: HomefeedRepo,
      ) : ViewModelProvider.Factory {
          override fun <T : ViewModel> create(modelClass: Class<T>): T {
              if (modelClass.isAssignableFrom(HomefeedViewModel::class.java)) {
                  @Suppress("UNCHECKED_CAST")
                  return HomefeedViewModel(homefeedRepo) as T
              }
              throw IllegalArgumentException("Unknown ViewModel class")
          }
      }

      构建ViewModel

      kotlin
      // 注意看这一行的代码,这里面疯狂嵌套new了一大堆的东西,非常复杂以及嵌套非常深。
      val homefeedRepo = HomefeedRepo(HomefeedUseCase(HomefeedService()))
      val factory = HomefeedViewModelFactory(homefeedRepo)
      val viewModel = ViewModelProvider(this, factory)[HomefeedViewModel::class.java]
      • 这种疯狂嵌套的构造函数,看了头皮都要发麻了,复杂程度非常大。

2) Service Locator(工厂/注册表)—— 注意它是有争议的模式

思想:全局注册/查找依赖(例如单例注册),调用方从容器拿依赖。比手动组装少些样板,但会隐藏依赖(不利于可测试性/明确的依赖契约)。

kotlin
object ServiceLocator {
    private val singletons = mutableMapOf<Class<*>, Any>()
    fun <T : Any> register(clazz: Class<T>, impl: T) { singletons[clazz] = impl }
    @Suppress("UNCHECKED_CAST")
    fun <T> get(clazz: Class<T>) = singletons[clazz] as T
}

// 初始化(Application)
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        ServiceLocator.register(UserRepository::class.java, UserRepositoryImpl(UserApi()))
    }
}

// 使用
class SomeClass {
    private val repo: UserRepository = ServiceLocator.get(UserRepository::class.java)
}

注意:Service Locator 会使依赖变隐式(难以在构造函数里看到),测试时需要替换全局状态,通常不推荐做为首选。


3) 使用 Dagger / Hilt(Google 推荐,适合中大型项目)

推荐理由:编译时注入、性能好、与 Android 生命周期集成(Hilt)——生产环境里最常用。下面给出 Hilt 的核心示例(Kotlin):

Gradle 依赖(简略)

gradle
// project build.gradle + app build.gradle 必要的 Hilt 依赖与 kapt
implementation "com.google.dagger:hilt-android:2.x"
kapt "com.google.dagger:hilt-android-compiler:2.x"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0" // 可选
kapt "androidx.hilt:hilt-compiler:1.0.0"

代码示例

kotlin
// Application
@HiltAndroidApp
class MyApplication : Application()

// 接口与实现
interface UserRepository { fun getUser(id: String): User }

class UserRepositoryImpl @Inject constructor(
    private val api: UserApi
): UserRepository {
    override fun getUser(id: String) = api.fetch(id)
}

// 提供外部依赖的 Module
@Module
@InstallIn(SingletonComponent::class) // Application 单例作用域
object NetworkModule {
    @Provides
    @Singleton
    fun provideUserApi(): UserApi = UserApi()

    @Provides
    @Singleton
    fun provideUserRepository(api: UserApi): UserRepository =
        UserRepositoryImpl(api)
}

// ViewModel 注入
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel()

// Activity 注入
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val vm: UserViewModel by viewModels()
}

要点

  • @HiltAndroidApp:启动 Hilt
  • @Module + @Provides@Binds 提供依赖
  • @InstallIn(...) 指定作用域(SingletonComponent、ActivityComponent、ViewModelComponent 等)
  • 构造器注入(@Inject constructor)是推荐方式
  • Hilt 也支持 @Binds(将接口绑定到实现,避免创建实例)

优点:自动生成依赖图、生命周期/作用域管理、便于测试(可替换 module)、性能优秀。 缺点:学习曲线有点,配置繁琐(但 Hilt 大幅简化 Dagger 的样板)。


4) 轻量级容器:Koin(DSL 风格,运行时注入)

思想:用 Kotlin DSL 声明依赖,启动时加载 module,运行时解析。简单、直观,适合中小型项目或快速开发。

kotlin
// Gradle: implementation "io.insert-koin:koin-android:3.x"

// AppModule.kt
val appModule = module {
    single { UserApi() }                // 单例
    single<UserRepository> { UserRepositoryImpl(get()) } // get() 注入 UserApi
    viewModel { UserViewModel(get()) }  // viewModel DSL
}

// Application
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApplication)
            modules(appModule)
        }
    }
}

// Activity
class MainActivity : AppCompatActivity() {
    private val vm: UserViewModel by viewModel()
}

优点:学习门槛低、DSL 可读性好、快速迭代。 缺点:运行时解析代价、在大型复杂项目中不如 Dagger/Hilt 严格。


DIP 在代码中的具体体现(模式)

  • 接口(或抽象类)声明依赖:interface UserRepository。高层(ViewModel/Presenter)只依赖接口。
  • 在外部(Module/Factory/Application)把低层实现绑定到接口(provideUserRepository / @Binds / Koin 的 single<UserRepository> { ... })。
  • 通过构造器注入把接口注入到高层模块:class UserViewModel(private val repo: UserRepository)。 这实现了“高层不依赖低层细节,细节依赖抽象”。

注入方式对比(优选顺序)

  1. 构造器注入(优先)—— 明确、易测试、DI 框架友好。
  2. 属性/字段注入(在某些框架下用,例如 Dagger/Hilt 对 Activity/Fragment)—— 可用,但隐藏依赖。
  3. 方法注入(较少见)—— 在需要延迟注入时有用。
  4. Service Locator(避免)—— 隐式依赖,不利于可测试性与可维护性。

测试友好策略

  • 在设计接口后,编写 Fake/Mock 实现供测试注入(手动或由测试版 Module 覆盖 Hilt Module / Koin module)。
  • Hilt 提供 @TestInstallIn 能替换 Module 来注入假实现。Koin 可以在测试时 load different modules。

实战建议(什么时候用哪个)

  • 小型 App / PoC / 学习:手动 DI 或 Koin(快速、简单)。
  • 中大型 App / 团队项目 / 产品级别:Hilt(与 Android 生命周期集成、编译时检查、可扩展性强)。
  • 注重性能 & 编译时安全:Dagger(或 Hilt,Hilt 是 Dagger 的封装)。
  • 避免:直接在类里 new 具体实现 —— 这违背 DIP/IoC。

简短 checklist (实战中要看)

  • 是否用接口抽象关键依赖?(是 → OK)
  • 是否在构造函数暴露依赖?(优先)
  • 是否使用 DI 框架管理作用域(Activity/Fragment/ViewModel/Singleton)?
  • 是否为测试提供替换实现?(通过 Module/ServiceLocator/test-only setup)

随便写写的,喜欢就好。