好的,作为一名在Android领域摸爬滚打十年的老兵,我来为你详细展开 模块化在实际开发中的落地细节。很多开发者对模块化的理解停留在“拆分成多个module”,但在实际项目中,如何划分、如何配置、如何管理依赖、如何保证编译速度,处处都是学问。下面我会结合真实的项目经验,带你一步步掌握模块化的实战技巧。
一、模块化的核心思想
在深入细节之前,我们先明确模块化的目标:将一个庞大的单体应用,按照一定的边界拆分成多个独立的Gradle模块,每个模块都有清晰的职责,可以独立编译、独立测试,甚至独立运行。最终目的是提升编译速度、降低耦合、便于团队协作。
二、模块划分的原则
模块划分没有标准答案,但遵循一些基本原则可以让架构更健康。通常我们会将模块分为三层:
1. 基础层(Base / Common)
- 包含完全与业务无关的基础能力,例如:
- 网络库封装(Retrofit + OkHttp)
- 图片加载(Glide / Coil)
- 工具类(日期处理、文件操作)
- 自定义View
- 基础Base类
- 特点:高度稳定,极少改动,被所有上层模块依赖。
2. 业务层(Feature / Business)
- 按照业务功能划分,每个业务一个模块,例如:
feature_home(首页)feature_mine(个人中心)feature_order(订单)
- 特点:高内聚,低耦合。业务模块之间不允许直接依赖,只能通过基础层或公共服务层通信。
- 如果业务模块需要独立运行(用于开发调试),需要配置成可独立运行的application模块。
3. 公共服务层(Service / Bridge)
- 这是组件化的产物,用于解耦业务模块。通常包括:
- 路由服务(如ARouter)
- 业务接口定义(如
ILoginService) - 事件总线定义
- 特点:被多个业务模块依赖,但本身不包含实现,只定义协议。
划分示例
MyApp/
├── app/ # 主模块,负责组装和初始化
├── buildSrc/ # 统一版本管理(或使用version catalog)
├── common/ # 基础层
│ ├── common-base # 最基础的工具类
│ ├── common-network # 网络库封装
│ └── common-ui # 自定义UI组件
├── feature/ # 业务层
│ ├── feature_home
│ ├── feature_mine
│ └── feature_order
├── service/ # 公共服务层
│ ├── service_router # 路由声明
│ └── service_user # 用户服务接口
三、实际开发中的配置细节
1. 模块类型选择
- application:用于可独立运行的模块,如
app主模块,以及业务模块在独立调试模式时。 - library:用于被依赖的模块,如基础层、公共服务层。
在build.gradle中:
// 业务模块默认作为 library
plugins {
id 'com.android.library'
}
// 当需要独立运行时,可以动态切换
if (isRunAlone.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
isRunAlone 通常在gradle.properties中定义,每个业务模块可以有自己的开关。
2. 依赖管理
2.1 统一版本管理
- 旧方式:在根目录的
build.gradle中使用ext定义版本变量。 - 新方式(推荐):使用 Gradle Version Catalog(Gradle 7.0+)或 buildSrc。
使用Version Catalog示例(gradle/libs.versions.toml):
[versions]
kotlin = "1.9.0"
retrofit = "2.9.0"
[libraries]
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
okhttp = "com.squareup.okhttp3:okhttp:4.11.0"
[plugins]
android-application = { id = "com.android.application", version = "8.1.0" }
在模块中引用:
dependencies {
implementation libs.retrofit
implementation libs.okhttp
}
2.2 依赖配置的关键字
implementation:只在当前模块内部使用,不会泄露给其他模块。这是默认首选,可以加快编译(因为依赖不会传递)。api:会将依赖暴露给上层模块。用于基础层需要暴露接口的场景(如common-network中暴露Retrofit实例)。compileOnly:仅编译时需要,不会打包进APK。用于注解处理器或提供接口但不实现。runtimeOnly:仅运行时需要,编译时不需要。
原则:尽可能使用implementation,减少api的使用,避免不必要的依赖传递。
3. 模块独立运行配置
为了让业务模块可以独立开发调试,需要:
- 动态切换
application和library插件。 - 在
src/main下创建独立的AndroidManifest.xml(用于独立运行时的入口Activity和application)。 - 配置独立的
applicationId(通常加上后缀)。
示例目录结构:
feature_home/
├── src/
│ ├── main/
│ │ ├── java/...
│ │ ├── res/...
│ │ └── AndroidManifest.xml # 合并后的最终清单
│ └── debug/ # 仅独立运行时使用
│ └── AndroidManifest.xml # 包含入口Activity
在build.gradle中配置sourceSets:
android {
sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
// 独立运行时,使用debug目录下的清单合并
debug {
manifest.srcFile 'src/debug/AndroidManifest.xml'
}
}
}
4. 模块间通信
业务模块解耦后,如何通信?
- 页面跳转:使用路由框架,如 ARouter。通过注解定义路径,然后通过路由跳转,可以传递参数。
// 在目标Activity上注解 @Route(path = "/home/main") class HomeActivity : AppCompatActivity() // 跳转 ARouter.getInstance().build("/home/main").navigation() - 服务调用:定义接口,通过路由获取实现。
// 接口定义在service模块 interface IUserService { fun getUserName(): String } // 实现类在feature_mine模块 @Route(path = "/service/user", name = "用户服务") class UserServiceImpl : IUserService { override fun getUserName(): String = "张三" } // 调用方通过ARouter获取服务 val userService = ARouter.getInstance().navigation(IUserService::class.java) - 事件通知:使用
LiveData、Flow或事件总线(如EventBus),但要注意生命周期管理。
5. 资源冲突与命名
- 资源合并时可能出现冲突,建议为每个模块的资源文件名加上模块前缀,例如:
home_activity_main.xmlmine_btn_login.xml
- 使用
resourcePrefix约束(在Gradle中配置):
这样如果模块内不小心使用了非前缀资源(如android { resourcePrefix "home_" }activity_main.xml),编译会报错,强制规范。
6. Manifest合并问题
- 每个模块都有自己的
AndroidManifest.xml,最终会合并到主模块。注意权限声明、四大组件声明可能会合并冲突。 - 可以在主模块中通过
tools:replace覆盖不需要的属性。 - 如果业务模块独立运行,需要额外的
<application>标签和入口Activity,建议放在debug源集中,避免影响release打包。
四、模块化的优势与挑战
优势
- 编译速度:修改一个模块后,只需编译该模块及其依赖,增量编译快。
- 并行开发:团队可以各自负责一个模块,互不影响。
- 代码复用:多个App可以共用基础模块。
- 独立测试:模块可独立运行单元测试,甚至UI测试。
挑战
- 初始化复杂度:主模块需要初始化所有模块,可能涉及依赖顺序。
- 模块间通信成本:需要设计路由或服务层,增加代码量。
- 版本管理:多个模块的版本需要协调,避免冲突。
- 构建脚本维护:需要统一管理依赖版本和插件。
五、实战技巧与最佳实践
1. 如何避免循环依赖
- 定期使用Gradle任务检查依赖图:
./gradlew :app:dependencies。 - 遵循依赖方向:基础层 → 公共服务层 → 业务层 → 主模块。业务层之间不能相互依赖。
- 使用
gradle-consistent插件或ArchUnit编写测试,在CI中自动检测循环依赖。
2. 编译优化
- 启用配置缓存(Gradle 5.0+):
# gradle.properties org.gradle.configuration-cache=true - 开启并行编译:
org.gradle.parallel=true - 使用构建缓存(包括远程缓存):
org.gradle.caching=true - 合理使用
implementationvsapi:减少传递依赖,加快编译。 - 按需配置:使用
includeBuild和复合构建,在开发时只引入需要的模块。
3. 共享资源与代码
- 资源:将公共资源(如颜色、样式、字符串)放在基础模块中,通过
android.resource方式引用。 - 代码:将公共的业务逻辑抽取到公共服务层,或者使用依赖注入(如Dagger Hilt)提供实例。
4. 自动化与脚本
- 使用版本目录统一管理依赖版本,避免手动修改多个文件。
- 创建自定义Gradle插件,自动化模块配置(例如统一设置
compileSdk、minSdk等)。 - 利用buildSrc或
convention plugins(Gradle 7.0+)封装通用构建逻辑。
convention plugins示例(在buildSrc或独立插件模块中):
// 自定义插件 MyLibraryPlugin.kt
class MyLibraryPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.apply("com.android.library")
project.extensions.configure<LibraryExtension> {
compileSdk = 34
defaultConfig {
minSdk = 21
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
}
}
然后在业务模块中应用:
plugins {
id("my-library-plugin")
id("org.jetbrains.kotlin.android")
}
5. 测试策略
- 单元测试:每个模块独立编写单元测试,mock依赖的外部模块。
- 集成测试:在主模块中编写,测试多个模块协作。
- 独立运行测试:业务模块独立运行时,可以编写针对该模块的UI测试。
六、常见错误与解决
错误1:资源命名冲突
- 现象:两个模块定义了相同名称的string或layout,导致合并时覆盖或编译错误。
- 解决:强制
resourcePrefix,并在代码审查中强调命名规范。
错误2:模块间直接依赖
- 现象:feature_home直接依赖feature_mine,导致循环依赖。
- 解决:重构代码,将需要共享的部分下沉到基础层或服务层。
错误3:依赖传递失控
- 现象:一个基础模块使用了
api暴露了大量库,导致所有业务模块都间接依赖了许多库,编译变慢。 - 解决:检查依赖树,将不必要的
api改为implementation,或创建单独的“api模块”仅暴露接口。
错误4:独立运行时与集成运行时的行为不一致
- 现象:业务模块独立运行正常,集成到主App后崩溃。
- 解决:确保独立运行时使用的依赖版本与主App一致;检查主App的Application初始化是否覆盖了模块的初始化逻辑。
七、总结
模块化是Android大型项目的基础,它不仅仅是技术拆分,更是工程管理和团队协作的基石。在实际落地中,我们需要:
- 合理划分模块,遵循依赖方向。
- 统一版本和构建配置,减少维护成本。
- 设计清晰的通信方式,解耦业务模块。
- 优化编译速度,提升开发体验。
- 建立规范和自动化检查,防止架构腐化。