Android 模块化 笔记

好的,作为一名在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
    • 事件总线定义
  • 特点:被多个业务模块依赖,但本身不包含实现,只定义协议

划分示例

csharp 复制代码
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中:

groovy 复制代码
// 业务模块默认作为 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):

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" }

在模块中引用:

groovy 复制代码
dependencies {
    implementation libs.retrofit
    implementation libs.okhttp
}

2.2 依赖配置的关键字

  • implementation:只在当前模块内部使用,不会泄露给其他模块。这是默认首选,可以加快编译(因为依赖不会传递)。
  • api:会将依赖暴露给上层模块。用于基础层需要暴露接口的场景(如common-network中暴露Retrofit实例)。
  • compileOnly:仅编译时需要,不会打包进APK。用于注解处理器或提供接口但不实现。
  • runtimeOnly:仅运行时需要,编译时不需要。

原则 :尽可能使用implementation,减少api的使用,避免不必要的依赖传递。

3. 模块独立运行配置

为了让业务模块可以独立开发调试,需要:

  • 动态切换applicationlibrary插件。
  • src/main下创建独立的AndroidManifest.xml(用于独立运行时的入口Activity和application)。
  • 配置独立的applicationId(通常加上后缀)。

示例目录结构:

bash 复制代码
feature_home/
├── src/
│   ├── main/
│   │   ├── java/...
│   │   ├── res/...
│   │   └── AndroidManifest.xml       # 合并后的最终清单
│   └── debug/                        # 仅独立运行时使用
│       └── AndroidManifest.xml        # 包含入口Activity

build.gradle中配置sourceSets:

groovy 复制代码
android {
    sourceSets {
        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
        }
        // 独立运行时,使用debug目录下的清单合并
        debug {
            manifest.srcFile 'src/debug/AndroidManifest.xml'
        }
    }
}

4. 模块间通信

业务模块解耦后,如何通信?

  • 页面跳转 :使用路由框架,如 ARouter 。通过注解定义路径,然后通过路由跳转,可以传递参数。

    kotlin 复制代码
    // 在目标Activity上注解
    @Route(path = "/home/main")
    class HomeActivity : AppCompatActivity()
    
    // 跳转
    ARouter.getInstance().build("/home/main").navigation()
  • 服务调用 :定义接口,通过路由获取实现。

    kotlin 复制代码
    // 接口定义在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)
  • 事件通知 :使用LiveDataFlow或事件总线(如EventBus),但要注意生命周期管理。

5. 资源冲突与命名

  • 资源合并时可能出现冲突,建议为每个模块的资源文件名加上模块前缀,例如:

    • home_activity_main.xml
    • mine_btn_login.xml
  • 使用resourcePrefix约束(在Gradle中配置):

    groovy 复制代码
    android {
        resourcePrefix "home_"
    }

    这样如果模块内不小心使用了非前缀资源(如activity_main.xml),编译会报错,强制规范。

6. Manifest合并问题

  • 每个模块都有自己的AndroidManifest.xml,最终会合并到主模块。注意权限声明、四大组件声明可能会合并冲突。
  • 可以在主模块中通过tools:replace覆盖不需要的属性。
  • 如果业务模块独立运行,需要额外的<application>标签和入口Activity,建议放在debug源集中,避免影响release打包。

四、模块化的优势与挑战

优势

  1. 编译速度:修改一个模块后,只需编译该模块及其依赖,增量编译快。
  2. 并行开发:团队可以各自负责一个模块,互不影响。
  3. 代码复用:多个App可以共用基础模块。
  4. 独立测试:模块可独立运行单元测试,甚至UI测试。

挑战

  1. 初始化复杂度:主模块需要初始化所有模块,可能涉及依赖顺序。
  2. 模块间通信成本:需要设计路由或服务层,增加代码量。
  3. 版本管理:多个模块的版本需要协调,避免冲突。
  4. 构建脚本维护:需要统一管理依赖版本和插件。

五、实战技巧与最佳实践

1. 如何避免循环依赖

  • 定期使用Gradle任务检查依赖图:./gradlew :app:dependencies
  • 遵循依赖方向:基础层 → 公共服务层 → 业务层 → 主模块。业务层之间不能相互依赖。
  • 使用gradle-consistent插件或ArchUnit编写测试,在CI中自动检测循环依赖。

2. 编译优化

  • 启用配置缓存 (Gradle 5.0+):

    gradle 复制代码
    # gradle.properties
    org.gradle.configuration-cache=true
  • 开启并行编译

    ini 复制代码
    org.gradle.parallel=true
  • 使用构建缓存 (包括远程缓存):

    ini 复制代码
    org.gradle.caching=true
  • 合理使用implementation vs api:减少传递依赖,加快编译。

  • 按需配置 :使用includeBuild和复合构建,在开发时只引入需要的模块。

3. 共享资源与代码

  • 资源 :将公共资源(如颜色、样式、字符串)放在基础模块中,通过android.resource方式引用。
  • 代码:将公共的业务逻辑抽取到公共服务层,或者使用依赖注入(如Dagger Hilt)提供实例。

4. 自动化与脚本

  • 使用版本目录统一管理依赖版本,避免手动修改多个文件。
  • 创建自定义Gradle插件,自动化模块配置(例如统一设置compileSdkminSdk等)。
  • 利用buildSrcconvention plugins(Gradle 7.0+)封装通用构建逻辑。

convention plugins示例 (在buildSrc或独立插件模块中):

kotlin 复制代码
// 自定义插件 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"
            }
        }
    }
}

然后在业务模块中应用:

gradle 复制代码
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大型项目的基础,它不仅仅是技术拆分,更是工程管理和团队协作的基石。在实际落地中,我们需要:

  1. 合理划分模块,遵循依赖方向。
  2. 统一版本和构建配置,减少维护成本。
  3. 设计清晰的通信方式,解耦业务模块。
  4. 优化编译速度,提升开发体验。
  5. 建立规范和自动化检查,防止架构腐化。
相关推荐
城东米粉儿1 小时前
Android HandlerThread 笔记
android
城东米粉儿2 小时前
Android Condition 笔记
android
肖。35487870942 小时前
html中onclick误区,后续变量会更改怎么办?
android·java·javascript·css·html
城东米粉儿2 小时前
Android 动态加载 Activity
android
城东米粉儿2 小时前
Android lancet 笔记
android
zh_xuan3 小时前
React Native 原生和RN互相调用以及事件监听
android·javascript·react native
哈哈浩丶4 小时前
LK(little kernel)-3:LK的启动流程-作为Android的bootloarder
android·linux·服务器
Android系统攻城狮12 小时前
Android tinyalsa深度解析之pcm_get_delay调用流程与实战(一百一十九)
android·pcm·tinyalsa·音频进阶·android hal·audio hal
·云扬·14 小时前
MySQL基于位点的主从复制完整部署指南
android·mysql·adb