乐于分享
好东西不私藏

大型 App 架构演进与模块化、组件化实践(3):组件化:模块化的延伸与独立运行

本文最后更新于2026-03-10,某些文章具有时效性,若有错误或已失效,请在下方留言或联系老夜

大型 App 架构演进与模块化、组件化实践(3):组件化:模块化的延伸与独立运行

大型 App 架构演进与模块化、组件化实践(3):组件化:模块化的延伸与独立运行

本文是「大型 App 架构演进与模块化、组件化实践」系列的第 3 篇,共 3 篇。在上一篇中,我们探讨了「模块化策略:大卸八块的艺术」的相关内容。

四、组件化:模块化的延伸与独立运行

组件化可以看作是模块化的一种高级形态或目标。它不仅仅是将代码拆分到不同模块,更强调将这些模块视为可独立开发、测试、甚至运行的“组件”。

  1. 核心理念:每个组件(通常是一个 Feature 模块)都应该具备独立运行和被组装的能力。

  2. 关键实践
    API/Implementation 分离:组件对外暴露的功能严格通过接口定义(通常放在独立的 :api 模块中),实现细节封装在内部(:impl 模块)。依赖方只依赖 :api 模块。
    面向接口编程:组件间的交互尽可能通过接口进行。
    依赖注入:组件内部的依赖(如 Repository、Context)通过 DI 容器从外部注入,而不是在内部创建。
    独立运行能力(Debug App):为每个组件创建一个小型的 application 模块(Debug App),它只依赖该组件及其必要的 Core 模块。这样可以单独编译、运行和测试该组件,极大地提高开发和调试效率。
    生命周期管理:需要考虑组件自身的生命周期与宿主环境(如 Application)生命周期的协调。

五、路由与通信

模块化后,原本简单的Activity跳转或方法调用变得不再直接。

  1. 挑战:Feature A 不能直接引用 Feature B 中的 Activity 类或 Service 类。

  2. 解决方案——路由框架(Router Frameworks)
    代表:ARouter(Alibaba)、WMRouter(Meituan Dianping)、TheRouter(货拉拉)、CC(JD)等。虽然具体实现有差异,但核心思想类似。
    工作原理(通用模式)

    1. 注册:在需要被外部访问的组件(Activity、Fragment、Service 实现类,甚至某个方法)上添加注解(如 @Route@Autowired)。
    2. 编译期处理:Annotation Processor 扫描注解,生成映射关系代码或配置文件,记录路径(如”/user/profile”)与目标组件类(com.example.feature.profile.UserProfileActivity)的对应关系。
    3. 运行时调用:业务代码通过 Router 框架提供的 API,使用路径字符串发起导航请求(如 Router.getInstance().build("/user/profile").withInt("userId", 123).navigation())。
    4. 查找与执行:Router 框架根据路径查找编译期生成的映射信息,找到目标类,创建 Intent(或直接调用方法),完成跳转或执行。
      核心功能
      页面路由:Activity、Fragment 跳转。
      服务调用:获取其他模块提供的服务接口实现(类似 ServiceLoader 但更强大)。
      参数传递与注入:通过 withXxx() 传递参数,通过 @Autowired 注解自动注入到目标页面。
      拦截器(Interceptor):在路由执行前后添加逻辑,如登录状态检查、权限申请、埋点等。
      降级策略:路由失败时的处理逻辑。
      思考与选型
      选择合适的框架:考虑其维护状态、社区活跃度、性能(反射 vs. 生成代码)、易用性、功能完备性。
      理解内部机制:了解其是基于编译期代码生成还是运行时反射/类加载,这对性能和稳定性有影响。
      路径管理:如何规范和管理大量的路由路径字符串,避免冲突和硬编码。
      类型安全:路径参数传递通常是基于 Bundle 或基本类型,缺乏编译期类型安全检查。
      调试:路由问题有时难以调试,需要熟悉框架的日志和错误处理。
      与 Instant Run/Apply Changes 的兼容性:部分基于 APT 的框架可能与 IDE 的热/冷更新功能有兼容性问题。
  3. 其他通信方式
    接口下沉 + DI:将 Feature B 需要暴露给 Feature A 的功能定义成接口,放在两者都依赖的 Core 或 API 模块中。Feature B 实现该接口,并通过 DI 框架(如 Hilt 的 @Binds @IntoSet)将其实现绑定到一个集合中。Feature A 注入这个接口集合,查找并调用。更类型安全,但可能引入更多接口和 DI 配置。
    广播(BroadcastReceiver):适用于一对多的事件通知,但耦合松散,数据传递能力弱,难以追踪,不推荐作为常规的模块间通信或导航手段。
    EventBus(如 EventBus、RxBus):强烈不推荐在大型模块化项目中使用。全局事件总线使得数据流向极其混乱,难以追踪和调试,是许多难以排查的 bug 的根源。

六、模块化下的依赖注入(DI)

DI是实现模块解耦的关键。

  1. 挑战:如何在模块不知晓具体实现的情况下,获取其所需的依赖对象(如 Repository、DataSource、Analytics Logger)?如何管理跨模块的依赖图?

  2. DI 框架的应用
    Dagger / Hilt

    • 跨模块依赖:通过 Dagger 的Component Dependencies机制。下游模块的 Component 可以依赖上游模块 Component 暴露的接口,从而获取上游提供的对象。
    • 聚合贡献@Binds @IntoSet/@IntoMap):不同模块可以向同一个全局或父 Component 贡献接口的实现(放入一个 Set 或 Map 中),供其他模块注入和使用。常用于插件化扩展点、路由表构建等。
    • Hilt 简化:Hilt 通过预定义的 Component 和 @InstallIn 注解简化了模块化下的 DI 设置,但仍需理解其 Component 层级和注入机制。@EntryPoint 可用于从非 Hilt 管理类中获取依赖。
    • 实践要点:设计合理的 Component 层级和 Scope;管理好 Component 依赖关系;利用好 Multi-binding;关注 KAPT/KSP 对编译速度的影响。
    • Koin
    • 模块化支持:Koin 通过其 Module DSL 提供了加载和卸载模块的能力(loadKoinModulesunloadKoinModules)。不同 Gradle 模块可以定义自己的 Koin Module,在 Application 启动时统一加载。
    • 动态性:可以在运行时动态加载/卸载 Koin 模块,更灵活。
    • 实践考量:运行时解析的开销;缺乏编译期安全检查;跨模块依赖关系不如 Dagger 直观;大型项目中的 Module 管理。

七、Gradle 依赖管理

模块数量增多后,依赖管理变得复杂。

  1. api vs implementation
    implementation(默认):依赖只在当前模块编译和运行时可见,不会传递给依赖当前模块的其他模块。优点:隐藏内部实现细节,减少不必要的传递依赖,提高编译速度(下游模块无需因上游 implementation 依赖的改变而重新编译)。
    api:依赖会传递给依赖当前模块的其他模块。使用场景:当一个模块的公开 API(类、方法、接口)直接使用了另一个模块的类型时,必须使用 api 暴露该类型。应尽可能少用 api。

  2. 统一版本管理
    Platform / BOM(Bill of Materials):推荐方式。引入一个 BOM 依赖,它定义了一组相关库的版本,无需在每个库后面单独指定版本。如 platform('androidx.compose:compose-bom:2024.03.00')
    Version Catalogs(libs.versions.toml):Gradle 7.0+ 推荐。在项目根目录的 gradle/libs.versions.toml 文件中集中定义所有库的坐标和版本别名。在 build.gradle(.kts) 中使用别名引用依赖(如 libs.androidx.core.ktx)。优点:极佳的可维护性、代码补全支持、易于共享。
    ext 块(传统方式):在根 build.gradle 中定义版本号变量。易用但不如 Version Catalog 规范。

  3. Gradle Convention Plugins(约定插件)
    目的:将通用的构建逻辑(如应用特定插件 com.android.library,配置 compileSdkminSdktestOptions,添加公共依赖如 Kotlin stdlib、JUnit)封装到自定义的 Gradle 插件中。
    优点:避免在每个模块的 build.gradle 中重复配置;保证构建配置的一致性;便于统一修改。是大型多模块项目的必备实践。

八、多模块项目的编译性能优化

这是模块化的核心收益之一,但也需要持续关注和优化。

  1. 利用 Gradle 特性
    Configuration Cache--configuration-cache):缓存配置阶段的结果,极大加速后续构建的配置过程。
    Build Cache--build-cache):缓存任务的输出,避免重复执行未改变的任务。使用本地缓存,并考虑搭建远程构建缓存供团队共享。
    Parallel Executionorg.gradle.parallel=true):允许多个任务并行执行。

  2. 优化模块本身
    增量编译:确保代码和资源修改能触发增量编译。
    增量注解处理:使用支持增量的 Annotation Processor(查看其文档)。优先使用 KSP(Kotlin Symbol Processing)替代 KAPT,KSP 通常更快且支持增量更佳。
    减少 api 依赖:如前述,implementation 有助于避免不必要的模块重编译。
    使用纯 Java/Kotlin 模块:对于不依赖 Android 框架的代码,使用 java-librarykotlin("jvm") 模块,编译速度更快。

  3. 构建配置优化
    按需配置(Configuration on demand):(Gradle 已默认或不再推荐显式开启)只配置当前任务需要的项目。
    避免在配置阶段执行耗时操作build.gradle 中的逻辑应尽可能简单。
    升级 Gradle 和 AGP:新版本通常包含性能改进。

  4. 分析与监控
    Gradle Build Scans--scan):上传构建信息到 Gradle Enterprise(或本地 Docker 镜像),提供详细的构建分析报告,包括任务耗时、依赖解析、瓶颈分析等。强烈推荐使用!
    Gradle Profiler--profile):生成本地 HTML 报告,分析任务执行时间。
    监控 CI 构建时间:跟踪构建时间变化趋势,及时发现并解决性能劣化问题。

九、模块化架构下的测试策略

模块化为不同层级的测试提供了更好的隔离性。

  1. 单元测试(Unit Tests)
    范围:单个类或方法。
    优势:在模块化下更容易实现。特别是对于纯 Java/Kotlin 的 Domain 层和 Data 层模块,可以完全在 JVM 上快速执行。Presentation 层的 ViewModel 也可以通过 Mock 依赖进行测试。
    实践:使用 JUnit、Mockito/MockK。

  2. 集成测试(Integration Tests)
    范围:测试同一模块内或跨模块(通过接口)的多个组件的交互。例如,测试 ViewModel -> Use Case -> Repository 的完整流程。
    环境:可以在 JVM 上运行(使用 Fake/Mock 实现外部依赖,如数据库、网络),也可以在 Android 设备/模拟器上运行(如果需要 Android 框架 API)。
    实践:使用 JUnit、Mockito/MockK、Robolectric(JVM 上模拟 Android 环境)、Espresso(设备上)。

  3. UI 测试 / 端到端测试(End-to-End Tests)
    范围:模拟用户操作,测试完整的用户流程,通常涉及 UI 交互。
    优势(模块化下)

    • 组件级 UI 测试:可以为每个 Feature 模块创建 Debug App,只包含该模块的 UI 和必要的依赖(用 Fake/Mock 替代其他 Feature),在该独立环境中运行 Espresso 测试。优点:运行速度快、稳定性高、独立性强。
    • 完整 App E2E 测试:在最终组装好的 App 上运行,覆盖跨多个 Feature 的流程。数量应相对较少,覆盖核心路径即可,因为它们运行慢且不稳定。
    • 实践:使用 Espresso、UI Automator。通过 DI 或特定 Test Runner 注入 Fake/Mock 依赖(如 MockWebServer 模拟网络请求)。

十、团队与流程适配

技术架构的演进必须伴随着团队结构和开发流程的调整。

  1. 代码所有权(Code Ownership):按模块(尤其是 Feature 模块)划分明确的团队或个人所有权,提高责任感和维护效率。

  2. 代码规范与审查(Code Style & Review):统一的代码规范和严格的 Code Review 对于保证跨模块代码质量至关重要。关注模块 API 的设计和演进。

  3. 分支策略(Branching Strategy):模块化使得基于特性的分支(Feature Branching)更加可行和独立。无论是 Gitflow 还是 Trunk-Based Development,都需要适应模块化的结构。

  4. CI/CD 优化:持续集成/持续部署流水线可以根据代码变更范围进行优化,例如只构建和测试受影响的模块及其依赖项,减少 CI 运行时间。

  5. API 契约与沟通:模块间的接口(API 模块、路由路径、数据契约)成为关键的「契约」。API 的设计、评审、版本管理和变更通知需要建立清晰的流程和沟通机制。

  6. 技术债务管理:模块化本身可能引入新的技术债务(如路由框架的维护、DI 配置的复杂度)。需要持续关注和偿还。

十一、结论:架构演进,永无止境

对于大型 Android 应用而言,从单体走向模块化、组件化是应对规模化挑战、提升工程效率和保障应用质量的必由之路。这并非一蹴而就的技术替换,而是一个涉及架构模式思辨、模块化策略权衡、技术栈(路由、DI、构建系统)精通、以及团队流程协同的系统性工程演进过程。

技术专家/Leader 在此过程中扮演着关键的设计者、决策者和推动者角色。需要深刻理解各种方案的优劣和适用场景,在编译速度、运行时性能、代码隔离性、开发效率、类型安全等多个维度间做出明智的权衡;需要掌握从 Gradle 优化到高级 DI 模式,再到跨模块通信的各种底层技术细节;还需要具备推动团队接受变革、建立规范、持续改进的领导力。

模块化/组件化本身并非终点,它带来的新问题(如通信复杂性、依赖管理挑战)也需要持续解决。架构演进是一个永无止境的循环,目标始终是构建一个更能适应未来变化、更能支撑业务发展、更能让开发者愉悦工作的、高质量的软件系统。


「大型 App 架构演进与模块化、组件化实践」系列目录

  1. 引言:应对规模化的必然演进
  2. 模块化策略:大卸八块的艺术
  3. 组件化:模块化的延伸与独立运行(本文)
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 大型 App 架构演进与模块化、组件化实践(3):组件化:模块化的延伸与独立运行

评论 抢沙发

8 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮