系列三:组件化与模块化进阶 | 第8篇
组件化与模块化核心实战区别:大型项目架构的必由之路(企业级全案)
阅读警告
本文为超深度技术长文,预计阅读时长 40-60 分钟,代码量极大。
在前七篇中,我们解决了**"代码怎么写"的问题(架构思想、MVVM、状态管理)。
从这一篇开始,我们要解决 "代码放在哪"和 "团队怎么协作"**的问题。如果你的项目编译一次要 5 分钟,改一行代码要等半天,或者两个人同时改代码天天冲突,那么这一篇就是为你写的。
我们将彻底厘清 模块化(Modularization) 与 组件化(Componentization) 的区别,并从零搭建一套 可独立运行、可插拔、可并行编译、可灰度发布 的企业级工程架构。
全文包含:Gradle 黑魔法、路由源码级剖析、资源隔离方案、组件生命周期管理、以及大厂落地血泪史。
1 引子:单体工程的死亡螺旋
让我们先看一个典型的"巨型工程"在 2 年后的样子。这不是虚构,这是 90% 成长型公司的必经之路。
1.1 症状诊断
- 编译速度的黑洞:全量编译从 1 分钟变成 10 分钟。因为任何一个模块的改动,Gradle 都会认为整个 App 需要重新编译。开发者的时间成本呈指数级上升。
- Git 冲突的噩梦 :
app模块下有 50 个开发人员同时在改,每天合并代码时,冲突文件多达几十个。AndroidManifest.xml永远是冲突的重灾区。 - 业务耦合的毒瘤:登录模块调用了支付模块的类,支付模块又依赖了商品模块的资源。代码像意大利面条一样纠缠在一起。想删一个功能?不敢删,因为不知道删了哪个地方会崩。
- 测试的地狱:改了一个工具类,回归测试要跑遍所有业务线。测试团队永远在加班。
- 发布的枷锁:一个业务线出了紧急 Bug,必须全量发包。没办法只更新某一个业务模块。
1.2 单体 vs 组件化 对比表
| 维度 | 单体工程 (Monolith) | 组件化工程 (Component) |
|---|---|---|
| 编译速度 | 慢(全量编译,10分钟+) | 快(增量编译,只编改动的模块,1分钟) |
| 并行开发 | 难(互相阻塞,Git 冲突多) | 易(每人负责一个组件,互不干扰) |
| 代码边界 | 模糊(互相引用,无强制约束) | 清晰(通过路由通信,物理隔离) |
| 独立调试 | 必须跑整个 App(启动慢) | 组件可单独运行(秒启) |
| 版本迭代 | 牵一发动全身(全量回归) | 组件可独立发版(灰度发布) |
| 技术栈升级 | 风险极大(牵一发而动全身) | 风险可控(单个组件试点) |
2 核心概念辨析:模块化 vs 组件化
这是 90% 的团队都会混淆的概念。请务必花 5 分钟理解透彻,这是后续所有架构的基石。
2.1 模块化(Modularization):按"技术职能"拆
定义 :将 App 拆分成多个 Library Module 。这些 Module 通常按 技术职能 划分,目的是为了代码复用。
例子:
module-network(网络封装:Retrofit、OkHttp、拦截器)module-database(数据库操作:Room、GreenDAO)module-utils(工具类:StringUtil、DateUtil)module-ui(自定义 View、Style、Theme)module-base(BaseActivity、BaseViewModel、BaseApplication)
特点:
- 不能独立运行(没有 Application,没有 Launcher Activity)。
- 依赖关系:业务层依赖基础模块。
- 目的:结构清晰,避免重复造轮子。
架构图:
#mermaid-svg-gq73EkDSPq4aAwQK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gq73EkDSPq4aAwQK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gq73EkDSPq4aAwQK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gq73EkDSPq4aAwQK .error-icon{fill:#552222;}#mermaid-svg-gq73EkDSPq4aAwQK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gq73EkDSPq4aAwQK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gq73EkDSPq4aAwQK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gq73EkDSPq4aAwQK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gq73EkDSPq4aAwQK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gq73EkDSPq4aAwQK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gq73EkDSPq4aAwQK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gq73EkDSPq4aAwQK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gq73EkDSPq4aAwQK .marker.cross{stroke:#333333;}#mermaid-svg-gq73EkDSPq4aAwQK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gq73EkDSPq4aAwQK p{margin:0;}#mermaid-svg-gq73EkDSPq4aAwQK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gq73EkDSPq4aAwQK .cluster-label text{fill:#333;}#mermaid-svg-gq73EkDSPq4aAwQK .cluster-label span{color:#333;}#mermaid-svg-gq73EkDSPq4aAwQK .cluster-label span p{background-color:transparent;}#mermaid-svg-gq73EkDSPq4aAwQK .label text,#mermaid-svg-gq73EkDSPq4aAwQK span{fill:#333;color:#333;}#mermaid-svg-gq73EkDSPq4aAwQK .node rect,#mermaid-svg-gq73EkDSPq4aAwQK .node circle,#mermaid-svg-gq73EkDSPq4aAwQK .node ellipse,#mermaid-svg-gq73EkDSPq4aAwQK .node polygon,#mermaid-svg-gq73EkDSPq4aAwQK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gq73EkDSPq4aAwQK .rough-node .label text,#mermaid-svg-gq73EkDSPq4aAwQK .node .label text,#mermaid-svg-gq73EkDSPq4aAwQK .image-shape .label,#mermaid-svg-gq73EkDSPq4aAwQK .icon-shape .label{text-anchor:middle;}#mermaid-svg-gq73EkDSPq4aAwQK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gq73EkDSPq4aAwQK .rough-node .label,#mermaid-svg-gq73EkDSPq4aAwQK .node .label,#mermaid-svg-gq73EkDSPq4aAwQK .image-shape .label,#mermaid-svg-gq73EkDSPq4aAwQK .icon-shape .label{text-align:center;}#mermaid-svg-gq73EkDSPq4aAwQK .node.clickable{cursor:pointer;}#mermaid-svg-gq73EkDSPq4aAwQK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gq73EkDSPq4aAwQK .arrowheadPath{fill:#333333;}#mermaid-svg-gq73EkDSPq4aAwQK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gq73EkDSPq4aAwQK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gq73EkDSPq4aAwQK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gq73EkDSPq4aAwQK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gq73EkDSPq4aAwQK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gq73EkDSPq4aAwQK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gq73EkDSPq4aAwQK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gq73EkDSPq4aAwQK .cluster text{fill:#333;}#mermaid-svg-gq73EkDSPq4aAwQK .cluster span{color:#333;}#mermaid-svg-gq73EkDSPq4aAwQK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gq73EkDSPq4aAwQK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gq73EkDSPq4aAwQK rect.text{fill:none;stroke-width:0;}#mermaid-svg-gq73EkDSPq4aAwQK .icon-shape,#mermaid-svg-gq73EkDSPq4aAwQK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gq73EkDSPq4aAwQK .icon-shape p,#mermaid-svg-gq73EkDSPq4aAwQK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gq73EkDSPq4aAwQK .icon-shape .label rect,#mermaid-svg-gq73EkDSPq4aAwQK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gq73EkDSPq4aAwQK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gq73EkDSPq4aAwQK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gq73EkDSPq4aAwQK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 基础模块 (Library)
App Module (壳)
MainActivity
module-network
module-database
module-ui
module-base
2.2 组件化(Componentization):按"业务线"拆
定义 :将 App 拆分成多个 Business Component 。这些 Component 通常按 业务线 划分,目的是为了业务解耦。
例子:
component-login(登录业务:手机号登录、微信登录、注册)component-pay(支付业务:支付宝、微信、银联)component-home(首页业务:Feed流、Banner、导航)component-user(用户中心:个人信息、设置、收货地址)component-order(订单业务:列表、详情、物流)
特点:
- 可以独立运行(有自己的 Application,有自己的 Launcher Activity)。
- 不能直接互相依赖(通过路由跳转,通过接口下沉通信)。
- 目的:业务隔离,并行开发,独立发布。
架构图:
#mermaid-svg-1KwVLUmNyQtjzhq6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1KwVLUmNyQtjzhq6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1KwVLUmNyQtjzhq6 .error-icon{fill:#552222;}#mermaid-svg-1KwVLUmNyQtjzhq6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1KwVLUmNyQtjzhq6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1KwVLUmNyQtjzhq6 .marker.cross{stroke:#333333;}#mermaid-svg-1KwVLUmNyQtjzhq6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1KwVLUmNyQtjzhq6 p{margin:0;}#mermaid-svg-1KwVLUmNyQtjzhq6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1KwVLUmNyQtjzhq6 .cluster-label text{fill:#333;}#mermaid-svg-1KwVLUmNyQtjzhq6 .cluster-label span{color:#333;}#mermaid-svg-1KwVLUmNyQtjzhq6 .cluster-label span p{background-color:transparent;}#mermaid-svg-1KwVLUmNyQtjzhq6 .label text,#mermaid-svg-1KwVLUmNyQtjzhq6 span{fill:#333;color:#333;}#mermaid-svg-1KwVLUmNyQtjzhq6 .node rect,#mermaid-svg-1KwVLUmNyQtjzhq6 .node circle,#mermaid-svg-1KwVLUmNyQtjzhq6 .node ellipse,#mermaid-svg-1KwVLUmNyQtjzhq6 .node polygon,#mermaid-svg-1KwVLUmNyQtjzhq6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1KwVLUmNyQtjzhq6 .rough-node .label text,#mermaid-svg-1KwVLUmNyQtjzhq6 .node .label text,#mermaid-svg-1KwVLUmNyQtjzhq6 .image-shape .label,#mermaid-svg-1KwVLUmNyQtjzhq6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-1KwVLUmNyQtjzhq6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1KwVLUmNyQtjzhq6 .rough-node .label,#mermaid-svg-1KwVLUmNyQtjzhq6 .node .label,#mermaid-svg-1KwVLUmNyQtjzhq6 .image-shape .label,#mermaid-svg-1KwVLUmNyQtjzhq6 .icon-shape .label{text-align:center;}#mermaid-svg-1KwVLUmNyQtjzhq6 .node.clickable{cursor:pointer;}#mermaid-svg-1KwVLUmNyQtjzhq6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1KwVLUmNyQtjzhq6 .arrowheadPath{fill:#333333;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1KwVLUmNyQtjzhq6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1KwVLUmNyQtjzhq6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1KwVLUmNyQtjzhq6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1KwVLUmNyQtjzhq6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1KwVLUmNyQtjzhq6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1KwVLUmNyQtjzhq6 .cluster text{fill:#333;}#mermaid-svg-1KwVLUmNyQtjzhq6 .cluster span{color:#333;}#mermaid-svg-1KwVLUmNyQtjzhq6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1KwVLUmNyQtjzhq6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1KwVLUmNyQtjzhq6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-1KwVLUmNyQtjzhq6 .icon-shape,#mermaid-svg-1KwVLUmNyQtjzhq6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1KwVLUmNyQtjzhq6 .icon-shape p,#mermaid-svg-1KwVLUmNyQtjzhq6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1KwVLUmNyQtjzhq6 .icon-shape .label rect,#mermaid-svg-1KwVLUmNyQtjzhq6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1KwVLUmNyQtjzhq6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1KwVLUmNyQtjzhq6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1KwVLUmNyQtjzhq6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 基础组件 (Foundation Modules)
业务组件 (Business Components)
Shell App (壳工程)
空壳 Application
component-login
component-pay
component-home
component-order
module-network
module-storage
module-widget
module-base
2.3 终极关系图(企业标准)
双层架构(这是大厂的标准答案):
- 底层(Level 1):基础模块(Modularization)。纯技术能力,无业务逻辑。
- 中层(Level 2):业务组件(Componentization)。纯业务逻辑,独立运行。
- 顶层(Level 3):壳工程(Shell)。空壳,只负责组装和配置。
#mermaid-svg-lSGKwqcXnER9QnsE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lSGKwqcXnER9QnsE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lSGKwqcXnER9QnsE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lSGKwqcXnER9QnsE .error-icon{fill:#552222;}#mermaid-svg-lSGKwqcXnER9QnsE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lSGKwqcXnER9QnsE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lSGKwqcXnER9QnsE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lSGKwqcXnER9QnsE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lSGKwqcXnER9QnsE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lSGKwqcXnER9QnsE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lSGKwqcXnER9QnsE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lSGKwqcXnER9QnsE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lSGKwqcXnER9QnsE .marker.cross{stroke:#333333;}#mermaid-svg-lSGKwqcXnER9QnsE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lSGKwqcXnER9QnsE p{margin:0;}#mermaid-svg-lSGKwqcXnER9QnsE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lSGKwqcXnER9QnsE .cluster-label text{fill:#333;}#mermaid-svg-lSGKwqcXnER9QnsE .cluster-label span{color:#333;}#mermaid-svg-lSGKwqcXnER9QnsE .cluster-label span p{background-color:transparent;}#mermaid-svg-lSGKwqcXnER9QnsE .label text,#mermaid-svg-lSGKwqcXnER9QnsE span{fill:#333;color:#333;}#mermaid-svg-lSGKwqcXnER9QnsE .node rect,#mermaid-svg-lSGKwqcXnER9QnsE .node circle,#mermaid-svg-lSGKwqcXnER9QnsE .node ellipse,#mermaid-svg-lSGKwqcXnER9QnsE .node polygon,#mermaid-svg-lSGKwqcXnER9QnsE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lSGKwqcXnER9QnsE .rough-node .label text,#mermaid-svg-lSGKwqcXnER9QnsE .node .label text,#mermaid-svg-lSGKwqcXnER9QnsE .image-shape .label,#mermaid-svg-lSGKwqcXnER9QnsE .icon-shape .label{text-anchor:middle;}#mermaid-svg-lSGKwqcXnER9QnsE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lSGKwqcXnER9QnsE .rough-node .label,#mermaid-svg-lSGKwqcXnER9QnsE .node .label,#mermaid-svg-lSGKwqcXnER9QnsE .image-shape .label,#mermaid-svg-lSGKwqcXnER9QnsE .icon-shape .label{text-align:center;}#mermaid-svg-lSGKwqcXnER9QnsE .node.clickable{cursor:pointer;}#mermaid-svg-lSGKwqcXnER9QnsE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lSGKwqcXnER9QnsE .arrowheadPath{fill:#333333;}#mermaid-svg-lSGKwqcXnER9QnsE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lSGKwqcXnER9QnsE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lSGKwqcXnER9QnsE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lSGKwqcXnER9QnsE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lSGKwqcXnER9QnsE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lSGKwqcXnER9QnsE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lSGKwqcXnER9QnsE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lSGKwqcXnER9QnsE .cluster text{fill:#333;}#mermaid-svg-lSGKwqcXnER9QnsE .cluster span{color:#333;}#mermaid-svg-lSGKwqcXnER9QnsE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lSGKwqcXnER9QnsE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lSGKwqcXnER9QnsE rect.text{fill:none;stroke-width:0;}#mermaid-svg-lSGKwqcXnER9QnsE .icon-shape,#mermaid-svg-lSGKwqcXnER9QnsE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lSGKwqcXnER9QnsE .icon-shape p,#mermaid-svg-lSGKwqcXnER9QnsE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lSGKwqcXnER9QnsE .icon-shape .label rect,#mermaid-svg-lSGKwqcXnER9QnsE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lSGKwqcXnER9QnsE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lSGKwqcXnER9QnsE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lSGKwqcXnER9QnsE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Level 1: 基础模块 (Foundation Modules)
module-network (网络)
module-storage (存储)
module-widget (UI)
module-base (基类)
module-router (路由)
Level 2: 业务组件 (Business Components)
component-login
component-pay
component-order
component-mine
component-im
Level 3: 壳工程 (Shell App)
app-shell (空壳)
3 实战:从零搭建组件化工程(手把手,含 Gradle 黑魔法)
现在,我们动手把一个单体工程拆成组件化工程。请跟着我的步骤操作。
3.1 第一步:工程目录规划
创建一个干净的 Project,目录结构如下:
ProjectRoot/
├── app-shell/ # 壳工程(空壳,只组装)
│ └── src/main/java/
│ └── AppShell.kt
│
├── components/ # 业务组件(可独立运行)
│ ├── component-login/
│ │ ├── src/main/java/
│ │ ├── src/debug/java/ # 独立运行时的配置
│ │ └── build.gradle
│ ├── component-home/
│ ├── component-pay/
│ └── component-mine/
│
├── modules/ # 基础模块(不可独立运行)
│ ├── module-base/ # 基类、路由、工具
│ ├── module-network/ # 网络封装
│ ├── module-storage/ # 数据库、SP
│ └── module-ui/ # 自定义 View、Style
│
├── build.gradle
├── settings.gradle
└── gradle.properties
3.2 第二步:创建模块(Gradle 配置)
在 settings.gradle 中注册所有模块。这是总控开关。
gradle
// settings.gradle.kts
include(":app-shell")
include(":component-login")
include(":component-home")
include(":component-pay")
include(":component-mine")
include(":module-base")
include(":module-network")
include(":module-storage")
include(":module-ui")
3.3 第三步:定义组件的"独立运行"开关(核心黑魔法)
这是组件化的灵魂。我们需要一个开关,控制组件是 独立运行 (开发时)还是 集成运行(打包时)。
在 gradle.properties 中定义全局变量:
properties
# 组件独立运行开关
# true = 独立运行(开发时,有 Application 和 Launcher)
# false = 集成运行(打包时,作为 Library 被壳工程依赖)
isLoginComponentDebug = true
isHomeComponentDebug = false
isPayComponentDebug = false
isMineComponentDebug = false
3.4 第四步:配置组件的 build.gradle
这是最关键的一步。组件需要根据开关切换 application 和 library 插件。
component-login/build.gradle.kts:
kotlin
plugins {
// 根据开关动态应用插件
if (isLoginComponentDebug.toBoolean()) {
id("com.android.application")
} else {
id("com.android.library")
}
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.component.login"
compileSdk = 34
defaultConfig {
minSdk = 21
// 只有作为 Application 时才需要 applicationId
if (isLoginComponentDebug.toBoolean()) {
applicationId = "com.example.component.login"
}
targetSdk = 34
}
// 源集配置:区分 debug 和 release
sourceSets {
getByName("main") {
// Manifest 文件分两套
if (isLoginComponentDebug.toBoolean()) {
manifest.srcFile("src/debug/AndroidManifest.xml")
} else {
manifest.srcFile("src/main/AndroidManifest.xml")
}
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
dependencies {
implementation(project(":module-base")) // 依赖基础模块
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
}
3.5 第五步:壳工程(Shell)的配置
壳工程是一个空的 Application,只负责组装组件。它不写任何业务逻辑。
app-shell/build.gradle.kts:
kotlin
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.appshell"
compileSdk = 34
defaultConfig {
applicationId = "com.example.appshell"
minSdk = 21
targetSdk = 34
}
}
dependencies {
implementation(project(":module-base"))
// 根据开关依赖组件
// 注意:如果组件是独立运行模式,壳工程就不能依赖它
if (!isLoginComponentDebug.toBoolean()) {
implementation(project(":component-login"))
}
if (!isHomeComponentDebug.toBoolean()) {
implementation(project(":component-home"))
}
if (!isPayComponentDebug.toBoolean()) {
implementation(project(":component-pay"))
}
if (!isMineComponentDebug.toBoolean()) {
implementation(project(":component-mine"))
}
}
3.6 第六步:Manifest 的隔离策略
组件作为 Library 时,不能有 applicationId,也不能有自己的 Launcher Activity。
1. 组件的公共 Manifest (component-login/src/main/AndroidManifest.xml):
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 注意:没有 applicationId -->
<application>
<!-- 这里只注册组件内的 Activity,不要写 Launcher -->
<activity
android:name=".LoginActivity"
android:exported="true" />
</application>
</manifest>
2. 组件的 Debug Manifest (component-login/src/debug/AndroidManifest.xml):
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".LoginApplication"
android:allowBackup="true"
android:label="登录组件(Debug)">
<!-- 独立运行时的入口 -->
<activity
android:name=".LoginActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
3. 壳工程的 Manifest (app-shell/src/main/AndroidManifest.xml):
xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:name=".AppShell"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
<!-- 壳工程的唯一入口 -->
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
4 组件通信:ARouter 源码级剖析与实战
组件不能互相依赖,那怎么跳转?答案是 路由(Router) 。市面上路由框架很多,但 ARouter 是阿里出品,最成熟,也是大厂标配。
4.1 为什么不用 Intent 隐式跳转?
- 无法传递复杂对象:Intent 只能传基本类型和 Serializable/Parcelable。像 Bitmap、自定义对象集合很难传。
- 无法获取返回值 :
StartActivityForResult在组件化下很难用,因为不知道目标 Activity 的类名。 - URL 硬编码 :
Intent intent = new Intent("com.example.login.LoginActivity")容易写错,且重构困难。 - 无法拦截:无法统一做登录校验、权限校验、埋点。
4.2 ARouter 的核心原理(源码级)
ARouter 通过 注解处理器(APT) 在编译期生成映射表。
流程详解:
-
编译期(APT):
- 扫描所有
@Route(path = "/login/activity")。 - 生成类
ARouter$$Group$$login,里面有一个HashMap<String, RouteMeta>。 - Key 是
"/login/activity",Value 是LoginActivity.class。
- 扫描所有
-
运行期(Init):
ARouter.init(application)被调用。- 通过反射加载所有生成的
ARouter$$Group$$*类。 - 把 HashMap 加载到内存中。
-
运行期(Navigation):
ARouter.getInstance().build("/login/activity").navigation()。- 在内存 Map 中查找
"/login/activity"对应的 Class。 - 调用
startActivity(new Intent(context, LoginActivity.class))。
流程图:
ARouter 内存映射表 注解处理器 开发者 ARouter 内存映射表 注解处理器 开发者 #mermaid-svg-V9vbTHg034YK0kj4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V9vbTHg034YK0kj4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V9vbTHg034YK0kj4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V9vbTHg034YK0kj4 .error-icon{fill:#552222;}#mermaid-svg-V9vbTHg034YK0kj4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V9vbTHg034YK0kj4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V9vbTHg034YK0kj4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V9vbTHg034YK0kj4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V9vbTHg034YK0kj4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V9vbTHg034YK0kj4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V9vbTHg034YK0kj4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V9vbTHg034YK0kj4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V9vbTHg034YK0kj4 .marker.cross{stroke:#333333;}#mermaid-svg-V9vbTHg034YK0kj4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V9vbTHg034YK0kj4 p{margin:0;}#mermaid-svg-V9vbTHg034YK0kj4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V9vbTHg034YK0kj4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-V9vbTHg034YK0kj4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-V9vbTHg034YK0kj4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-V9vbTHg034YK0kj4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-V9vbTHg034YK0kj4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-V9vbTHg034YK0kj4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-V9vbTHg034YK0kj4 .sequenceNumber{fill:white;}#mermaid-svg-V9vbTHg034YK0kj4 #sequencenumber{fill:#333;}#mermaid-svg-V9vbTHg034YK0kj4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-V9vbTHg034YK0kj4 .messageText{fill:#333;stroke:none;}#mermaid-svg-V9vbTHg034YK0kj4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V9vbTHg034YK0kj4 .labelText,#mermaid-svg-V9vbTHg034YK0kj4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-V9vbTHg034YK0kj4 .loopText,#mermaid-svg-V9vbTHg034YK0kj4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-V9vbTHg034YK0kj4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-V9vbTHg034YK0kj4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-V9vbTHg034YK0kj4 .noteText,#mermaid-svg-V9vbTHg034YK0kj4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-V9vbTHg034YK0kj4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V9vbTHg034YK0kj4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V9vbTHg034YK0kj4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-V9vbTHg034YK0kj4 .actorPopupMenu{position:absolute;}#mermaid-svg-V9vbTHg034YK0kj4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-V9vbTHg034YK0kj4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-V9vbTHg034YK0kj4 .actor-man circle,#mermaid-svg-V9vbTHg034YK0kj4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-V9vbTHg034YK0kj4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 写 @Route(path="/login/activity")生成 Map {"/login/activity": LoginActivity.class}ARouter.init()加载 Map 到内存build("/login/activity").navigation()查找 Class返回 LoginActivity.classstartActivity(LoginActivity)
4.3 实战:集成 ARouter(企业级配置)
1. 基础模块 module-base 中添加依赖(作为统一出口):
gradle
// module-base/build.gradle.kts
dependencies {
// Arouter API
implementation("io.github.alibaba:arouter-api:1.5.2")
// 注意:Compiler 不能放在 base 里,因为每个组件都要用自己的 Compiler
}
2. 每个业务组件中添加 Compiler 依赖:
gradle
// component-login/build.gradle.kts
dependencies {
// Arouter Compiler (注解处理器)
kapt("io.github.alibaba:arouter-compiler:1.5.2")
}
3. 初始化(壳工程中):
kotlin
// app-shell/AppShell.kt
class AppShell : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
ARouter.openLog()
ARouter.openDebug() // 开启调试模式(如果在 Instant Run 模式下运行,必须开启)
}
ARouter.init(this)
}
}
4. 使用(跳转与传参):
kotlin
// 跳转
ARouter.getInstance()
.build("/pay/activity")
.withString("orderId", "123456")
.withInt("price", 999)
.navigation()
// 接收参数
@Route(path = "/pay/activity")
class PayActivity : AppCompatActivity() {
@Autowired(name = "orderId")
lateinit var orderId: String
@Autowired(name = "price")
var price: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ARouter.getInstance().inject(this) // 自动注入
Log.d("PayActivity", "orderId=$orderId, price=$price")
}
}
4.4 拦截器(企业级核心:登录校验)
场景:未登录用户点击"我的订单",直接跳转到登录页。
kotlin
@Interceptor(priority = 8, name = "登录拦截器")
class LoginInterceptor : IInterceptor {
override fun process(postcard: Postcard?, callback: InterceptorCallback?) {
val path = postcard?.path
// 需要登录的页面
if (path == "/order/activity" || path == "/pay/activity") {
if (!UserManager.isLogin) {
// 中断路由,跳转到登录
ARouter.getInstance().build("/login/activity").navigation()
callback?.onInterrupt(RuntimeException("未登录"))
return
}
}
// 放行
callback?.onContinue(postcard)
}
override fun init(context: Context?) {}
}
5 资源隔离与冲突解决(大坑预警)
组件化最大的坑不是代码,而是 资源。资源冲突会导致编译直接失败,或者运行时出现诡异的样式错乱。
5.1 资源命名冲突
如果组件 A 和组件 B 都有一个 btn_confirm.xml,编译时会报错:Resource entry is already defined。
解决方案 :强制资源前缀(Resource Prefix)。
在 gradle.properties 中配置:
properties
# component-login
resourcePrefix = login_
# component-pay
resourcePrefix = pay_
# component-home
resourcePrefix = home_
在 build.gradle 中强制:
gradle
android {
resourcePrefix 'login_'
}
命名规范(强制执行):
- Layout:
login_activity_main.xml,pay_activity_index.xml - Drawable:
login_ic_wechat.png,pay_bg_alipay.webp - String:
login_btn_confirm,pay_title_price - Style:
LoginTheme,PayButtonStyle
5.2 公共资源下沉
有些资源是全局通用的(如 colors.xml, styles.xml, ic_launcher.png, strings.xml 中的 App 名称)。
策略:
- 放在
module-base或module-common中。 - 组件依赖
module-base。 - 组件自己的资源只给自己用。
注意 :module-base 中的资源越少越好,否则会成为新的瓶颈。
5.3 Theme 隔离
每个组件可以有自己的 Theme,但最终要继承壳工程的 Theme。
xml
<!-- module-base/themes.xml -->
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- 全局定义:颜色、字体、形状 -->
<item name="colorPrimary">@color/base_color_primary</item>
</style>
<!-- component-login/themes.xml -->
<style name="LoginTheme" parent="BaseAppTheme">
<!-- 登录页特有:比如背景是白色 -->
<item name="android:windowBackground">@color/white</item>
</style>
6 组件初始化:Application 的拆分
以前我们只有一个 Application。现在组件化后,每个组件都需要初始化(如推送、地图、数据库、IM SDK)。
问题:组件没有 Application,怎么初始化?
6.1 方案一:ContentProvider(推荐,无侵入)
Android 在初始化 Application 时,会先初始化所有 ContentProvider。我们可以利用这个机制。
原理:
- 每个组件定义一个
InitProvider。 - App 启动时,系统自动调用所有 Provider 的
onCreate。
实现:
kotlin
// module-base/BaseInitProvider.kt
class BaseInitProvider : ContentProvider() {
override fun onCreate(): Boolean {
// 初始化基础库(网络、日志、数据库)
initBaseLibs()
return true
}
// ... 其他方法空实现
}
// component-login/LoginInitProvider.kt
class LoginInitProvider : ContentProvider() {
override fun onCreate(): Boolean {
// 初始化登录模块(推送、IM)
initLoginSdk()
return true
}
}
优点 :无侵入,自动调用。
缺点:Provider 过多会影响启动速度(需优化)。
6.2 方案二:接口代理(手动调用,可控)
定义一个初始化接口,壳工程启动时依次调用。
实现:
kotlin
// module-base/IComponentApplication.kt
interface IComponentApplication {
fun onCreate(app: Application)
fun onTerminate() {}
}
// component-login/LoginApplication.kt
class LoginApplication : IComponentApplication {
override fun onCreate(app: Application) {
initLoginSdk()
}
}
// AppShell.kt
class AppShell : Application() {
override fun onCreate() {
super.onCreate()
// 手动调用(可以通过反射,或者维护一个列表)
LoginApplication().onCreate(this)
PayApplication().onCreate(this)
HomeApplication().onCreate(this)
}
}
优点 :启动顺序可控,方便排查问题。
缺点:需要手动维护调用列表。
6.3 方案三:Jetpack Startup(官方推荐,替代 ContentProvider)
Google 推出了 App Startup 库,专门解决组件初始化问题。
实现:
kotlin
// module-base/BaseInitializer.kt
class BaseInitializer : Initializer<Unit> {
override fun create(context: Context) {
initBaseLibs()
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
// component-login/LoginInitializer.kt
class LoginInitializer : Initializer<Unit> {
override fun create(context: Context) {
initLoginSdk()
}
override fun dependencies(): List<Class<out Initializer<*>>> {
// 依赖 BaseInitializer,确保先初始化基础库
return listOf(BaseInitializer::class.java)
}
}
优点 :官方支持,性能好,依赖关系清晰。
缺点:需要引入新库。
7 企业级组件化工程结构(最终形态)
ProjectRoot/
├── app-shell/ # 壳工程(空壳,只组装)
│ └── src/main/java/
│ └── AppShell.kt
│
├── components/ # 业务组件(可独立运行)
│ ├── component-login/
│ │ ├── src/main/java/
│ │ │ └── LoginActivity.kt
│ │ ├── src/debug/java/ # 独立运行时的配置
│ │ └── build.gradle
│ ├── component-home/
│ ├── component-pay/
│ └── component-mine/
│
├── modules/ # 基础模块(不可独立运行)
│ ├── module-base/ # 基类、路由、工具
│ ├── module-network/ # 网络封装
│ ├── module-storage/ # 数据库、SP
│ └── module-ui/ # 自定义 View、Style
│
├── build.gradle
├── settings.gradle
└── gradle.properties
8 总结:组件化的"军规"
- 组件之间零依赖 :只能通过路由通信,不能
implementation project(:component-login)。 - 资源必须加前缀:防止冲突,这是红线。
- 基础模块下沉:通用代码往下沉,业务代码往上浮。
- 独立运行优先:开发时组件能独立跑,不依赖壳工程。
- 壳工程要薄:壳工程只做组装和全局配置,不包含业务逻辑。
- 初始化要收敛:统一用 Startup 或 Provider,不要在 Application 里写一堆 init。
下一篇预告 :
系列三:组件化与模块化进阶 | 第9篇:组件化架构从零搭建实战(Gradle 极速配置与编译加速)
我们将深入 Gradle 的 Configuration Cache、Build Cache、并行编译 ,把 10 分钟的编译缩短到 1 分钟以内。同时会讲 多环境配置(Dev/Test/Prod) 和 多渠道打包。
如果你的项目已经到了"编译一次去喝杯咖啡"的阶段,请把这篇转给技术负责人。组件化不是选择题,而是生存题。