多模块项目:多模块架构下的Koin配置与依赖管理
说实话,多模块架构在Android项目里已经不是什么新鲜事了。我这些年经手的项目,但凡规模大一点的,几乎都是多模块的。但每次接手一个新项目,看到Koin的配置方式,我都能感受到不同团队对依赖注入的理解差异有多大。
今天我们就来聊聊,在多模块架构下,Koin到底该怎么配置,模块间的依赖关系怎么管理。嗯,这里面的坑,我踩过不少。
为什么多模块下Koin配置会变复杂?
单模块项目里,你只需要一个Application类,把所有module都注册进去就行了。但多模块不一样——每个模块都有自己的依赖,模块之间还有依赖关系。你想想看,如果模块A依赖模块B的某个类,但模块B的Koin配置还没加载,那不就出问题了?
我曾经在一个项目中遇到过这样的情况:两个模块互相依赖,结果启动时直接崩溃,报错信息是"no definition found"。排查了半天,发现是模块加载顺序搞反了。
多模块Koin配置的核心原则
我个人习惯把Koin的配置分成三个层次:
| 层次 | 说明 | 示例 |
|---|---|---|
| 基础层 | 网络、数据库、日志等基础设施 | networkModule, databaseModule |
| 业务层 | 各业务模块的依赖 | userModule, orderModule |
| 表现层 | ViewModel、UI相关 | viewModelModule |
说白了,就是按照依赖的先后顺序来组织。基础层先加载,业务层后加载,表现层最后加载。这个顺序不能乱。
模块间依赖管理的几种方式
我见过团队用不同的方式处理模块间依赖,这里列出三种比较常见的:
方式一:统一注册
在app模块里,把所有子模块的Koin module都收集起来,统一注册。这种方式最简单,但耦合度高。
// app模块的Application类
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(
networkModule,
databaseModule,
userModule,
orderModule,
viewModelModule
)
}
}
}
这种方式的问题在于:app模块需要知道所有子模块的存在。如果项目有几十个模块,那这个列表会变得非常长。
方式二:模块内自注册
每个模块在自己的Application类里注册自己的依赖,然后通过某种机制让app模块知道。这种方式更解耦,但实现起来稍微复杂一些。
// user模块
class UserModuleInitializer : ModuleInitializer {
override fun getModules(): List<Module> = listOf(userModule)
}
// app模块
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
// 通过ServiceLoader或手动收集所有ModuleInitializer
modules(allModules)
}
}
}
方式三:按需加载
这种方式比较高级,适合大型项目。每个模块只在需要的时候才加载自己的依赖。
// 在Feature模块中
class FeatureModule {
fun loadDependencies() {
loadKoinModules(featureModule)
}
}
// 使用时
class FeatureActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
FeatureModule().loadDependencies()
super.onCreate(savedInstanceState)
}
}
这种方式的好处是启动速度快,但要注意:如果多个模块同时加载,可能会有并发问题。
模块间依赖的常见问题与解决方案
我在项目中遇到过几个典型问题,这里分享给大家:
模块A依赖模块B,模块B又依赖模块A。这种情况在Koin里会直接报错。
解决方案:引入一个公共模块C,把A和B共同依赖的部分放到C里。或者使用懒加载(lazy)来打破循环。
某个模块的依赖在另一个模块里定义,但加载顺序不对。
解决方案:确保基础模块先加载。我一般会在Application的onCreate里按顺序加载:先加载基础模块,再加载业务模块。
实战:一个典型的多模块Koin配置
下面是一个我实际项目中用过的配置结构,大家可以参考:
// 基础模块:网络层
val networkModule = module {
single { Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
single { get<Retrofit>().create(ApiService::class.java) }
}
// 基础模块:数据库层
val databaseModule = module {
single { Room.databaseBuilder(
get<Context>(),
AppDatabase::class.java,
"app.db"
).build() }
single { get<AppDatabase>().userDao() }
}
// 业务模块:用户模块
val userModule = module {
single { UserRepository(get(), get()) }
viewModel { UserViewModel(get()) }
}
// 业务模块:订单模块
val orderModule = module {
single { OrderRepository(get(), get()) }
viewModel { OrderViewModel(get()) }
}
// app模块:统一注册
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(
networkModule, // 先加载基础
databaseModule, // 再加载基础
userModule, // 然后业务
orderModule // 最后业务
)
}
}
}
多模块依赖关系图
为了让大家更直观地理解模块间的依赖关系,我画了一张图:
避坑指南
最后,分享几个我踩过的坑:
- 模块间不要有循环依赖——我曾经在两个模块之间搞了循环依赖,结果Koin直接崩溃。后来用了一个公共模块才解决。
- 注意单例的作用域——在多模块下,single的作用域是整个Koin容器。如果你希望某个单例只在某个模块内有效,可以考虑用scoped。
- 测试时要小心——多模块的测试比单模块复杂得多。我一般会为每个模块写独立的测试,用KoinTestRule来管理依赖。
好了,关于多模块下的Koin配置,今天就聊到这里。记住一个原则:按依赖顺序加载,避免循环依赖,保持模块间解耦。做到这三点,多模块的Koin配置就不会出大问题。