现代Android开发依赖注入框架:为何首选 Koin 而非 Hilt?

5,876 阅读8分钟

引言

在现代软件开发中,依赖注入(Dependency Injection, DI)已成为一种广泛使用的设计模式,能够有效解耦代码,提升模块化和可测试性。通过 DI,可以轻松管理依赖关系,避免手动实例化对象并显式传递依赖。Android 开发中,Hilt 和 Koin 是两大常见的 DI 框架。本文从集成难易、性能对比,跨平台性以及背后维护公司的角度,探讨为什么在现代Android开发中 Koin 更适合做为依赖注入框架。

什么是依赖注入?

依赖注入是一种设计模式,用于将对象的依赖从内部移到外部进行管理。它的主要好处包括:

  • 解耦代码:通过接口或抽象类注入依赖,降低模块之间的耦合度。
  • 提升测试性:可以使用 Mock 对象注入,方便单元测试。
  • 易于维护:通过集中管理依赖关系,减少手动创建对象的复杂性。

以下是一个没有使用 DI 的传统实现:

class UserRepository {
    fun getUser(): String = "User Data"
}

class UserService {
    private val userRepository = UserRepository()

    fun getUserInfo(): String {
        return userRepository.getUser()
    }
}

这种方式的问题在于 UserServiceUserRepository 的耦合度过高。而使用 DI,可以改为:

interface UserRepository {
    fun getUser(): String
}

class UserRepositoryImpl : UserRepository {
    override fun getUser(): String = "User Data"
}

class UserService(private val userRepository: UserRepository) {
    fun getUserInfo(): String {
        return userRepository.getUser()
    }
}

依赖通过构造函数传入,这种方式更灵活,方便测试和扩展。

什么是控制反转( IoC)?

说到依赖注入,我们常常会和控制反转这个概念弄混淆。确实这两个概念是相关的,因为实际上依赖注入是实现控制反转的一种手段。

控制反转(Inversion of Control,简称 IoC)是一种软件设计原则,旨在通过将对象的控制权从开发者控制转移到外部框架来实现解耦。这意味着对象的生命周期和依赖关系由框架或容器管理,而非由编码者自身管理。这种管理依赖的关系的控制权发生了反转,从自身交给了外部容器(框架)。比如Java中大名鼎鼎的Spring框架,就具有Ioc容器功能。

DI 是 IoC 的一种具体实现方式。它通过构造函数、方法参数或属性注入对象的依赖,从而实现控制反转。DI 框架(如 Hilt 或 Koin)可以自动管理依赖的创建和注入。

为什么要引入依赖注入?

在ViewModel中使用

先看看如果不用依赖注入框架,有一个ViewModel,现在需要给它传入不同的参数,我们会怎么做呢?

class UserViewModel(private var id: String?):ViewModel() {

为了给这个UserViewModel传入id参数,androidx的lifecycle库需要我们写一个继承自ViewModelProvider.Factory的工厂类,使用的是工厂设计模式,目的是提供一个能传参的ViewModel。

class UseViewModelFactory(private val id: String?) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
            return UserViewModel(id) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

自定义了一个UseViewModelFactory,我们才能在UI中使用viewModel函数创建出这个UserViewModel对象,整个过程比较繁琐。特别是UserViewModel中需要传入多个参数或者Repository接口的不同实现类的话,创建起来会更加麻烦。

@Composable
fun UserScreen(
    id: String?,
    viewModel: UserViewModel = viewModel(
        factory = UseViewModelFactory(
            id
        )
    ),
    onBackPressed: () -> Unit = {},
)

看看使用Koin,依赖注入的方式是不是简单很多呢?

我们先在Koin的module方法里,使用简洁的DSL声明一下依赖关系

    val userModule = module {
        viewModel { (id: String) -> UserViewModel(id) }
    }

然后在项目中的MyApplication类(继承自系统的Application类)的onCreate方法里初始化一下

    override fun onCreate() {
        super.onCreate()
        startKoin{
            androidLogger()
            androidContext(this@AppBridgeApplication)
            modules(userModule)
        }
    }

这样我们就能在Compose方法组件里面使用了

    @Composable
    fun UserScreen(
        id: String?,
         viewModel: UserViewModel = koinViewModel(
            parameters = { parametersOf(id) }
        ),
        onBackPressed: () -> Unit = {},
    )

可以看到使用Koin依赖注入,我们能省掉写一个自定义的ViewModel Factory类。使用koinViewModel方法,就能直接传入想要的参数,是不是非常的方便。

多模块间的通讯服务

而且Koin的功能不止是做依赖注入,还可以实现多模块项目间的通讯服务,在Base模块定义接口,在子模块实现。能做到和ARouter一样的跨模块API调用,达到各模块间的解耦。

比如说我们在Base模块定义了一个IUserService类,来获取用户的信息。

interface IUserService {
    fun getUserInfo(): Flow<Response>
}

然后我们在User模块去实现这个类:

class IUserServiceImpl: IUserService {
    override fun getUserInfo(): Flow<Response> {
        TODO("Not yet implemented")
    }
}

接着也需要在User模块定义Koin的module方法里申明IUserService类具体实现的子类是IUserServiceImpl

val userModule = module {
    single<IUserService> { IUserServiceImpl() }
}

然后我们就能在Base模块使用injectOrNull注入方法从Koin的依赖注入容器中创建一个IUserService接口,就能调用这个服务接口里的方法了,而不用去关心这个接口的具体实现子类是谁,实现了各模块间的解耦。

private val userService: IUserService? by injectOrNull(IUserService::class.java)
userService?.getUserInfo()?.collect{
}

感兴趣的同学可以去Koin的官网查看更多使用介绍。Koin 介绍文档

为什么选择Koin?

  1. 简单且对开发人员友好: Koin 干净的 DSL、无编译时开销、最少的设置和简单的测试让您可以专注于业务逻辑的实现。
  2. 非常的小巧: 不论是小型项目还是复杂项目,Koin都是可以轻松扩展以满足您的需求。
  3. 安全性检查: Koin运行时能够进行依赖对象合法性检查,而且它的IDE 插件也即将发布,以后也不需要再羡慕Hilt的IDE插件导航了。
  4. 支持Kotlin Multiplatform: Koin因为是纯Kotlin写的,可以无缝的管理 iOS、Android、桌面和 Web 上的依赖项,使其成为跨平台开发的首选 DI 框架。
  5. 非常适合Jetpack Compose: Koin 与Jetpack Compose集成起来非常轻松,支持界面组件( ViewModel)的依赖注入,未来实现迁移到iOS的Compose Multiplatform跨平台也是非常的方便。

对比Hilt

Koin无论是使用的便捷性上还是未来非常重要的跨平台性上都是完胜Hilt的,具体对比如下:

  1. 使用Hilt需要引入hilt-compiler插件,会增加编译时间,而Koin是不需要任何插件; Hilt 通过注解处理器(Annotation Processor)生成代码,增加了编译时间。Koin使用 Kotlin DSL代码配置依赖,无需生成额外代码,对编译时间不影响。

  2. Hilt需要学习各种注解,学习曲线高@HiltViewModel@Inject@Module,@AndroidEntryPoint等,配置起来比较麻烦,而Koin配置很直观,上手容易。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var repository: MyRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 使用 repository
    }
}
    @HiltViewModel
    class HotNewsViewModel @Inject constructor(private val openApiRepository: OpenApiRepository)

3. 性能对比上,国外有人把Android官方使用Hilt写的Now in Android 和使用Koin重写的Now in Android APP进行基准测试对比,发现差距很小。说明Koin的性能也不差,足以应对我们的要求。

屏幕截图 2025-01-17 155520.png 4. IDE插件上,Hilt给类加上注入注解,左侧会有快捷导航图标,点击能导航到该注入类被声明的地方,非常的方便;

图片.png Koin的IDE插件目前没有,不过也即将发布,大家可以期待一波。

图片描述
  1. 跨平台性: 因为Hilt是对Java写的Dagger2框架的移动端优化,要想实现像kotlin写的Koin一样支持Kotlin Multiplatform实现跨平台,并不容易,大量的代码需要重写,恐怕未来很长一段时间都将无法做到,更适合专注于Android APP开发的团队。

从未来企业降本增效的背景下,越来越多的公司的APP开发,都开始选择了跨平台,而且人员配备上,有些公司都裁员到只剩一个Android和一个iOS来负责维护公司APP的程度了。

Koin支持Kotlin Multiplatform,Koin依赖注入的代码可以在多个平台间共享,减少了重复开发。这样公司可以招两个Android一个iOS的APP团队,UI层分别使用各自原生的UI框架Jetpack Compose和Swift UI实现,业务逻辑使用KMP实现,既能保证用户的原生体验,又能节约成本。

  1. 官方支持: Hilt 是 Android Jetpack 的一部分,有Android官方的背书,文档说明很详细,Hilt介绍文档; Koin由Kotzilla Team和社区支持,也有广泛的群众基础,文档说明也很详尽,而且Kotzilla是Kotlin基金会的银牌会员,大家无需担心后期维护的问题。Koin 介绍文档

图片.png

总结

综合以上分析,以下是选择 Koin 的主要理由:

  1. 简单易用:无需生成代码,配置直观,易于学习。
  2. 跨平台支持:能够在多种平台上使用,适合未来APP开发的趋势。
  3. 轻量级:相较于 Hilt 的复杂性,Koin 更轻便,减少了维护成本。

现代Android开发,Koin无疑是更为合适的选择,新开发Android项目我建议直接上Koin。当然现有的Android项目也可以渐进式的开始使用Koin,来替代原来的Hilt,或者二者共存也是没有问题的,大家赶紧用起来吧。