模块化设计:Gradle多模块配置、模块间通信、资源隔离
模块化设计,说白了就是把一个庞大的应用拆成多个独立的小模块。每个模块各司其职,互不干扰。我刚开始做架构时,也踩过不少坑——一个模块改动,整个项目都要重新编译,那滋味可不好受。
今天我们就来聊聊模块化的三个核心问题:Gradle多模块怎么配、模块间怎么通信、资源怎么隔离。嗯,这三个问题搞定了,你的项目架构基本就稳了。
一、Gradle多模块配置
先说说多模块的目录结构。我个人习惯这样组织:
MyApp/
├── app/ # 主应用模块
├── common/ # 公共基础模块
├── module_login/ # 登录模块
├── module_home/ # 首页模块
├── module_mine/ # 我的模块
├── lib_network/ # 网络库模块
├── lib_ui/ # UI组件库模块
└── settings.gradle.kts # 模块注册文件
在 settings.gradle.kts 中注册模块:
rootProject.name = "MyApp"
include(":app")
include(":common")
include(":module_login")
include(":module_home")
include(":module_mine")
include(":lib_network")
include(":lib_ui")
每个模块的 build.gradle.kts 要统一管理依赖版本。我建议用 版本目录(Version Catalog) 的方式,而不是到处写死版本号。为什么?因为升级依赖时,你只需要改一个地方。
在 gradle/libs.versions.toml 中定义:
[versions]
kotlin = "1.9.0"
compose-bom = "2024.01.00"
retrofit = "2.9.0"
[libraries]
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
然后在模块中引用:
dependencies {
implementation(libs.retrofit.core)
implementation(libs.retrofit.gson)
}
build.gradle.kts 尽量保持简洁。公共配置可以抽取到 build-logic 模块中,用 convention plugins 统一管理。这样新加模块时,几行代码就搞定了。
二、模块间通信
模块间通信,这是个老生常谈的问题。你想想看,登录模块怎么告诉首页模块「用户登录成功了」?直接依赖?那模块化就白做了。
我常用的方案有三种:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 接口下沉 | 模块间有明确的调用关系 | 类型安全,编译期检查 | 需要定义公共接口模块 |
| 事件总线 | 一对多、松耦合的场景 | 解耦彻底,使用灵活 | 运行时风险,不易追踪 |
| 路由框架 | 页面跳转、跨模块调用 | 支持URL跳转,拦截器强大 | 需要额外引入框架 |
接口下沉是我最推荐的方式。具体做法是:
- 在
common模块中定义接口 - 业务模块实现接口
- 通过
ServiceLoader或依赖注入获取实现
举个例子:
// common模块中定义
interface ILoginService {
fun isLoggedIn(): Boolean
fun getUserId(): String
}
// login模块中实现
class LoginServiceImpl : ILoginService {
override fun isLoggedIn(): Boolean {
return SPUtils.getBoolean("is_login", false)
}
override fun getUserId(): String {
return SPUtils.getString("user_id", "")
}
}
// home模块中使用
class HomeViewModel @Inject constructor(
private val loginService: ILoginService
) {
fun checkLoginStatus() {
if (loginService.isLoggedIn()) {
// 展示用户信息
}
}
}
事件总线适合那些「通知一下就行」的场景。比如用户退出登录,多个模块都需要清理缓存。我用的是 SharedFlow,比传统的 EventBus 更轻量,也更 Kotlin 化。
// 定义事件
object Events {
data class LogoutEvent(val reason: String)
data class RefreshEvent(val pageId: Int)
}
// 发送事件
fun postLogout() {
eventBus.emit(Events.LogoutEvent("user_manual"))
}
// 接收事件
fun observeLogout() {
viewModelScope.launch {
eventBus.collect { event ->
when (event) {
is Events.LogoutEvent -> handleLogout(event.reason)
else -> {}
}
}
}
}
三、资源隔离
资源隔离,说白了就是防止模块间的资源命名冲突。你想想看,两个模块都定义了一个 ic_launcher.png,编译时到底用哪个?
我遇到过最坑的一次:登录模块和首页模块都定义了 color_primary,结果首页的按钮颜色莫名其妙变了。排查了半天,才发现是资源覆盖了。
解决方案其实很简单:给每个模块加资源前缀。
在 build.gradle.kts 中配置:
android {
namespace = "com.example.module_login"
resourcePrefix = "login_"
}
这样,登录模块的资源文件必须用 login_ 开头,比如 login_ic_avatar.png、login_color_primary。如果某个资源忘了加前缀,编译时会直接报错。嗯,这招很管用。
除了命名隔离,还有资源目录隔离。每个模块的资源放在自己的 src/main/res 下,不要共享。公共资源(比如主题、通用图标)放在 common 模块中。
目录结构示例:
module_login/
└── src/
└── main/
├── res/
│ ├── drawable/
│ │ └── login_ic_avatar.png
│ ├── layout/
│ │ └── login_activity.xml
│ ├── values/
│ │ ├── login_colors.xml
│ │ └── login_strings.xml
│ └── ...
└── ...
strings.xml 里定义了 app_name,结果覆盖了主模块的配置。从那以后,我要求所有模块的字符串资源都必须带模块前缀,比如 login_app_name、home_title。虽然名字长了点,但安全。
最后说说资源引用。模块间引用资源时,要通过 R 类来访问。但注意,如果模块 A 依赖了模块 B,模块 A 可以直接用 R.b.xxx。如果模块 A 不依赖模块 B,那就需要通过接口或事件来传递资源 ID。
举个例子:
// common模块定义接口
interface IResourceProvider {
fun getLoginAvatar(): Drawable
fun getLoginHint(): String
}
// login模块实现
class LoginResourceProvider : IResourceProvider {
override fun getLoginAvatar(): Drawable {
return ContextCompat.getDrawable(context, R.drawable.login_ic_avatar)!!
}
override fun getLoginHint(): String {
return context.getString(R.string.login_hint)
}
}
// home模块使用
class HomeViewModel @Inject constructor(
private val resourceProvider: IResourceProvider
) {
fun getAvatar() = resourceProvider.getLoginAvatar()
}
这样做的好处是:资源的使用者和提供者完全解耦。你换模块、换资源,都不影响其他模块。
好了,模块化设计的三个核心问题就聊到这里。总结一下:
- Gradle配置:用版本目录统一管理依赖,用 convention plugins 抽取公共配置
- 模块间通信:优先用接口下沉,事件总线只用于通知场景
- 资源隔离:加资源前缀,公共资源放 common 模块,通过接口传递资源
这些经验都是我在实际项目中一点点积累的。刚开始做模块化时,我也觉得麻烦,但坚持下来后,项目的可维护性、编译速度、团队协作效率都提升了一大截。嗯,值得投入。