Android 项目从几个人、几个页面增长到几十人、数百个页面后,最先暴露的问题通常不是某个类写得不好,而是变化无法被限制在局部:改一个公共模块触发全量编译,一个业务依赖另一个业务的内部实现,任何页面都能读取全局状态,发布时没人知道改动会影响谁。
模块化、组件化和插件化都是解决复杂度的手段,但它们解决的问题并不相同:
- 模块化关注编译期的代码边界;
- 组件化在模块边界上进一步建立业务自治、接口契约和组装机制;
- 插件化关注运行时动态发现、加载或替换代码与资源。
真正有效的架构,不是模块数量最多,而是用恰当的隔离成本换取可验证的收益。
1. 先用四个维度判断边界强度
讨论"一个东西是不是组件"之前,先问四个问题:
| 维度 | 核心问题 | 常见技术手段 |
|---|---|---|
| 编译独立 | 能否作为独立编译单元构建和测试? | Gradle Module、AAR/JAR |
| 依赖独立 | 是否只通过公开契约与其他部分协作? | internal、接口、依赖倒置 |
| 交付独立 | 是否可以独立发布、按需下载或替换? | Maven、Dynamic Feature、独立 APK |
| 运行独立 | 是否能在运行时动态发现、加载、卸载? | ClassLoader、资源加载、插件容器 |
这四种能力是逐渐增强、成本逐渐升高的。一个 Gradle Module 只天然拥有第一项,并不会自动获得业务自治或运行时动态能力。
#mermaid-svg-haI0ztCikEC269yg{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-haI0ztCikEC269yg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-haI0ztCikEC269yg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-haI0ztCikEC269yg .error-icon{fill:#552222;}#mermaid-svg-haI0ztCikEC269yg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-haI0ztCikEC269yg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-haI0ztCikEC269yg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-haI0ztCikEC269yg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-haI0ztCikEC269yg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-haI0ztCikEC269yg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-haI0ztCikEC269yg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-haI0ztCikEC269yg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-haI0ztCikEC269yg .marker.cross{stroke:#333333;}#mermaid-svg-haI0ztCikEC269yg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-haI0ztCikEC269yg p{margin:0;}#mermaid-svg-haI0ztCikEC269yg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-haI0ztCikEC269yg .cluster-label text{fill:#333;}#mermaid-svg-haI0ztCikEC269yg .cluster-label span{color:#333;}#mermaid-svg-haI0ztCikEC269yg .cluster-label span p{background-color:transparent;}#mermaid-svg-haI0ztCikEC269yg .label text,#mermaid-svg-haI0ztCikEC269yg span{fill:#333;color:#333;}#mermaid-svg-haI0ztCikEC269yg .node rect,#mermaid-svg-haI0ztCikEC269yg .node circle,#mermaid-svg-haI0ztCikEC269yg .node ellipse,#mermaid-svg-haI0ztCikEC269yg .node polygon,#mermaid-svg-haI0ztCikEC269yg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-haI0ztCikEC269yg .rough-node .label text,#mermaid-svg-haI0ztCikEC269yg .node .label text,#mermaid-svg-haI0ztCikEC269yg .image-shape .label,#mermaid-svg-haI0ztCikEC269yg .icon-shape .label{text-anchor:middle;}#mermaid-svg-haI0ztCikEC269yg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-haI0ztCikEC269yg .rough-node .label,#mermaid-svg-haI0ztCikEC269yg .node .label,#mermaid-svg-haI0ztCikEC269yg .image-shape .label,#mermaid-svg-haI0ztCikEC269yg .icon-shape .label{text-align:center;}#mermaid-svg-haI0ztCikEC269yg .node.clickable{cursor:pointer;}#mermaid-svg-haI0ztCikEC269yg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-haI0ztCikEC269yg .arrowheadPath{fill:#333333;}#mermaid-svg-haI0ztCikEC269yg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-haI0ztCikEC269yg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-haI0ztCikEC269yg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-haI0ztCikEC269yg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-haI0ztCikEC269yg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-haI0ztCikEC269yg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-haI0ztCikEC269yg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-haI0ztCikEC269yg .cluster text{fill:#333;}#mermaid-svg-haI0ztCikEC269yg .cluster span{color:#333;}#mermaid-svg-haI0ztCikEC269yg 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-haI0ztCikEC269yg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-haI0ztCikEC269yg rect.text{fill:none;stroke-width:0;}#mermaid-svg-haI0ztCikEC269yg .icon-shape,#mermaid-svg-haI0ztCikEC269yg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-haI0ztCikEC269yg .icon-shape p,#mermaid-svg-haI0ztCikEC269yg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-haI0ztCikEC269yg .icon-shape .label rect,#mermaid-svg-haI0ztCikEC269yg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-haI0ztCikEC269yg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-haI0ztCikEC269yg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-haI0ztCikEC269yg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Package
代码组织
Module
编译边界
Component
业务与契约边界
Dynamic Feature
按需交付
Plugin
运行时动态加载
越向右,隔离能力越强,同时构建、调试、测试、发布和治理成本也越高。
2. 从最小到最大的颗粒度
"颗粒度"不是文件数量,而是一个单元包含多少职责、由谁维护、以什么方式变化。
2.1 类与函数:实现颗粒度
最小边界是函数和类。它们通过可见性、接口和单一职责控制复杂度:
kotlin
internal class OrderPriceCalculator {
fun calculate(items: List<OrderItem>): Money = TODO()
}
此级别解决的是局部可读性和测试问题,不能限制其他 package 或 module 的编译依赖。
2.2 Package:代码组织颗粒度
Package 是命名和组织边界:
text
com.example.shop
order/
payment/
profile/
优点是成本低、移动方便。缺点是 Java/Kotlin package 不能天然阻止跨目录引用;如果所有类型都是 public,任何业务仍然能穿透边界。
对小项目,package by feature 往往是最合理的第一步:
text
app/src/main/java/com/example/shop/
order/
presentation/
domain/
data/
payment/
presentation/
domain/
data/
它先验证业务边界是否稳定,避免过早付出多模块构建成本。
2.3 Gradle Module:编译颗粒度
Gradle Module 是明确的编译单元,例如:
text
:app
:core:network
:core:database
:feature:order
:feature:payment
它可以:
- 独立配置依赖和 Android/Kotlin 插件;
- 独立执行单元测试和静态检查;
- 使用
internal隐藏模块内部实现; - 通过构建图限制允许的依赖方向;
- 在部分条件下提升增量编译和并行构建效率。
但它不自动保证:
- 模块职责合理;
- 公共 API 足够小;
- 业务可以独立运行;
- 团队不会把所有东西放进
common; - 构建一定更快。
2.4 Library:可复用交付颗粒度
Android Library 或 Kotlin/JVM Library 可以产出 AAR/JAR,并发布到 Maven 仓库。Library 更强调可复用和版本化:
text
group: com.example.platform
artifact: analytics
version: 2.4.0
一旦跨仓库或跨团队发布,就需要处理兼容性、语义化版本、弃用周期、变更日志和制品安全。并不是所有内部 Module 都值得变成独立 Library。
2.5 Feature Component:业务颗粒度
业务组件通常围绕一个稳定的业务能力形成,例如:
- 订单;
- 支付;
- 用户账号;
- 商品搜索;
- 消息会话。
成熟的业务组件不只是一个 Module,而是一组边界:
text
输入:路由参数、公开 Use Case、稳定领域类型
输出:结果、导航契约、领域事件
内部:UI、状态、业务逻辑、数据适配
所有权:明确团队或负责人
验证:组件级测试与依赖规则
组件的核心属性是高内聚、低耦合和可组装,不是一定能单独启动一个 App。
2.6 App / Product Component:产品颗粒度
同一代码库可能组装多个产品:
text
:app-consumer -> order + payment + profile
:app-merchant -> inventory + order-manage + analytics
:app-demo -> selected feature implementations
App 壳负责最终组装、进程级初始化、顶层导航、品牌配置和发布签名。它不应重新实现各业务内部逻辑。
2.7 Dynamic Feature:按需交付颗粒度
Dynamic Feature Module 可以由应用商店按需或条件交付。它解决 APK 初始体积和功能按需下载问题,但仍受主应用的签名、版本和官方交付机制管理。
它不是通用运行时插件系统:
- 通常与 Base App 一起构建和发布;
- 依赖图和资源在构建期已知;
- 生命周期仍由 Android 官方组件模型管理;
- 不等于从任意服务器下载未知代码执行。
2.8 Plugin:运行时扩展颗粒度
插件是运行时动态加载的扩展单元。宿主通过稳定协议发现插件,插件可以相对独立地提供代码、资源或功能。
这会引入 ClassLoader、资源访问、组件生命周期代理、版本协议、安全和平台限制等复杂问题,只有明确需要动态扩展时才值得采用。
3. 什么是模块化
3.1 定义
模块化是把系统拆成多个具有明确职责和依赖关系的编译单元。在 Android 中通常体现为 Gradle Module。
模块化的首要目标是:
- 限制源码可见性;
- 建立编译依赖图;
- 降低单个单元的认知负担;
- 支持局部构建、测试和复用。
3.2 模块类型
| 类型 | 示例 | 主要职责 |
|---|---|---|
| App Module | :app |
打包、组装、签名和产品配置 |
| Feature Module | :feature:order |
完整业务能力 |
| Core Module | :core:network |
跨业务基础能力 |
| Domain Module | :domain:order |
领域模型、规则和端口 |
| UI Module | :core:design-system |
设计令牌和可复用 UI |
| Test Module/Fixtures | :core:testing |
测试工具和 Fake |
| Build Logic | :build-logic |
Convention Plugin、统一构建规则 |
3.3 implementation 与 api
Gradle 依赖配置决定 API 是否向上传递:
kotlin
dependencies {
implementation(projects.core.network)
implementation(libs.kotlinx.coroutines.core)
}
优先使用 implementation,它能缩小编译 classpath,避免消费者无意依赖传递实现。
只有当依赖类型出现在模块公开 API 中、消费者必须直接访问时才使用 api:
kotlin
dependencies {
api(projects.core.model)
}
大量 api(project(...)) 往往意味着边界泄漏:底层改动沿依赖图向上传播,模块化的编译隔离价值会消失。
3.4 模块化不等于组件化
以下工程已经多模块化,但未必组件化:
text
:app -> :feature-order -> :feature-user -> :feature-home
如果订单模块直接导入用户模块的 Fragment、数据库 Entity 和内部 ViewModel,模块虽然分开编译,业务边界仍然耦合。
4. 什么是组件化
4.1 定义
组件化是在模块化基础上,以业务能力或稳定基础能力为边界,通过公开契约、依赖规则和组装机制形成可组合单元。
一个真正的组件至少应回答:
- 对外提供什么能力?
- 需要外部提供什么依赖?
- 哪些类型是公开契约?
- 内部实现能否在不影响调用者的情况下变化?
- 谁拥有它,如何测试和发布?
4.2 组件的结构
#mermaid-svg-XCz2MnGKaOeDziLn{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-XCz2MnGKaOeDziLn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XCz2MnGKaOeDziLn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XCz2MnGKaOeDziLn .error-icon{fill:#552222;}#mermaid-svg-XCz2MnGKaOeDziLn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XCz2MnGKaOeDziLn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XCz2MnGKaOeDziLn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XCz2MnGKaOeDziLn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XCz2MnGKaOeDziLn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XCz2MnGKaOeDziLn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XCz2MnGKaOeDziLn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XCz2MnGKaOeDziLn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XCz2MnGKaOeDziLn .marker.cross{stroke:#333333;}#mermaid-svg-XCz2MnGKaOeDziLn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XCz2MnGKaOeDziLn p{margin:0;}#mermaid-svg-XCz2MnGKaOeDziLn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XCz2MnGKaOeDziLn .cluster-label text{fill:#333;}#mermaid-svg-XCz2MnGKaOeDziLn .cluster-label span{color:#333;}#mermaid-svg-XCz2MnGKaOeDziLn .cluster-label span p{background-color:transparent;}#mermaid-svg-XCz2MnGKaOeDziLn .label text,#mermaid-svg-XCz2MnGKaOeDziLn span{fill:#333;color:#333;}#mermaid-svg-XCz2MnGKaOeDziLn .node rect,#mermaid-svg-XCz2MnGKaOeDziLn .node circle,#mermaid-svg-XCz2MnGKaOeDziLn .node ellipse,#mermaid-svg-XCz2MnGKaOeDziLn .node polygon,#mermaid-svg-XCz2MnGKaOeDziLn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XCz2MnGKaOeDziLn .rough-node .label text,#mermaid-svg-XCz2MnGKaOeDziLn .node .label text,#mermaid-svg-XCz2MnGKaOeDziLn .image-shape .label,#mermaid-svg-XCz2MnGKaOeDziLn .icon-shape .label{text-anchor:middle;}#mermaid-svg-XCz2MnGKaOeDziLn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XCz2MnGKaOeDziLn .rough-node .label,#mermaid-svg-XCz2MnGKaOeDziLn .node .label,#mermaid-svg-XCz2MnGKaOeDziLn .image-shape .label,#mermaid-svg-XCz2MnGKaOeDziLn .icon-shape .label{text-align:center;}#mermaid-svg-XCz2MnGKaOeDziLn .node.clickable{cursor:pointer;}#mermaid-svg-XCz2MnGKaOeDziLn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XCz2MnGKaOeDziLn .arrowheadPath{fill:#333333;}#mermaid-svg-XCz2MnGKaOeDziLn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XCz2MnGKaOeDziLn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XCz2MnGKaOeDziLn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XCz2MnGKaOeDziLn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XCz2MnGKaOeDziLn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XCz2MnGKaOeDziLn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XCz2MnGKaOeDziLn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XCz2MnGKaOeDziLn .cluster text{fill:#333;}#mermaid-svg-XCz2MnGKaOeDziLn .cluster span{color:#333;}#mermaid-svg-XCz2MnGKaOeDziLn 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-XCz2MnGKaOeDziLn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XCz2MnGKaOeDziLn rect.text{fill:none;stroke-width:0;}#mermaid-svg-XCz2MnGKaOeDziLn .icon-shape,#mermaid-svg-XCz2MnGKaOeDziLn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XCz2MnGKaOeDziLn .icon-shape p,#mermaid-svg-XCz2MnGKaOeDziLn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XCz2MnGKaOeDziLn .icon-shape .label rect,#mermaid-svg-XCz2MnGKaOeDziLn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XCz2MnGKaOeDziLn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XCz2MnGKaOeDziLn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XCz2MnGKaOeDziLn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 注入
调用方
Component Contract
Component Implementation
Domain / Use Cases
Data Adapters
App Composition Root
契约可以是路由、接口、输入输出模型或领域端口。调用方依赖契约,不依赖 Activity、DAO、DTO 等实现细节。
4.3 组件化的目标
- 业务变化被限制在组件内部;
- 多团队能够在清晰所有权下并行开发;
- 依赖关系可验证,减少循环依赖;
- 组件可以在不同 App 中组装;
- 内部实现可以替换或重构;
- 构建和测试可以按影响范围执行。
4.4 组件是否必须独立运行
不必须。让每个组件都能作为独立 App 启动,会额外维护 Manifest、Application、Mock 登录、导航和资源配置。
只有下列情况值得提供独立 Demo/Sandbox:
- UI 组件需要快速预览;
- 大型业务由独立团队维护;
- 主 App 启动成本高,严重影响调试;
- 组件有明确可模拟的输入边界。
更轻量的做法是建立统一 :app-sandbox,通过配置选择要挂载的 Feature,而不是给每个 Feature 复制一个 App。
5. 什么是插件化
5.1 定义
插件化让宿主在运行时发现并加载扩展实现。插件通常不参与宿主常规源码编译,双方通过稳定 SDK 或协议交互。
#mermaid-svg-lONSkC9XDFVUkH5m{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-lONSkC9XDFVUkH5m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lONSkC9XDFVUkH5m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lONSkC9XDFVUkH5m .error-icon{fill:#552222;}#mermaid-svg-lONSkC9XDFVUkH5m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lONSkC9XDFVUkH5m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lONSkC9XDFVUkH5m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lONSkC9XDFVUkH5m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lONSkC9XDFVUkH5m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lONSkC9XDFVUkH5m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lONSkC9XDFVUkH5m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lONSkC9XDFVUkH5m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lONSkC9XDFVUkH5m .marker.cross{stroke:#333333;}#mermaid-svg-lONSkC9XDFVUkH5m svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lONSkC9XDFVUkH5m p{margin:0;}#mermaid-svg-lONSkC9XDFVUkH5m .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lONSkC9XDFVUkH5m .cluster-label text{fill:#333;}#mermaid-svg-lONSkC9XDFVUkH5m .cluster-label span{color:#333;}#mermaid-svg-lONSkC9XDFVUkH5m .cluster-label span p{background-color:transparent;}#mermaid-svg-lONSkC9XDFVUkH5m .label text,#mermaid-svg-lONSkC9XDFVUkH5m span{fill:#333;color:#333;}#mermaid-svg-lONSkC9XDFVUkH5m .node rect,#mermaid-svg-lONSkC9XDFVUkH5m .node circle,#mermaid-svg-lONSkC9XDFVUkH5m .node ellipse,#mermaid-svg-lONSkC9XDFVUkH5m .node polygon,#mermaid-svg-lONSkC9XDFVUkH5m .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lONSkC9XDFVUkH5m .rough-node .label text,#mermaid-svg-lONSkC9XDFVUkH5m .node .label text,#mermaid-svg-lONSkC9XDFVUkH5m .image-shape .label,#mermaid-svg-lONSkC9XDFVUkH5m .icon-shape .label{text-anchor:middle;}#mermaid-svg-lONSkC9XDFVUkH5m .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lONSkC9XDFVUkH5m .rough-node .label,#mermaid-svg-lONSkC9XDFVUkH5m .node .label,#mermaid-svg-lONSkC9XDFVUkH5m .image-shape .label,#mermaid-svg-lONSkC9XDFVUkH5m .icon-shape .label{text-align:center;}#mermaid-svg-lONSkC9XDFVUkH5m .node.clickable{cursor:pointer;}#mermaid-svg-lONSkC9XDFVUkH5m .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lONSkC9XDFVUkH5m .arrowheadPath{fill:#333333;}#mermaid-svg-lONSkC9XDFVUkH5m .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lONSkC9XDFVUkH5m .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lONSkC9XDFVUkH5m .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lONSkC9XDFVUkH5m .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lONSkC9XDFVUkH5m .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lONSkC9XDFVUkH5m .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lONSkC9XDFVUkH5m .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lONSkC9XDFVUkH5m .cluster text{fill:#333;}#mermaid-svg-lONSkC9XDFVUkH5m .cluster span{color:#333;}#mermaid-svg-lONSkC9XDFVUkH5m 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-lONSkC9XDFVUkH5m .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lONSkC9XDFVUkH5m rect.text{fill:none;stroke-width:0;}#mermaid-svg-lONSkC9XDFVUkH5m .icon-shape,#mermaid-svg-lONSkC9XDFVUkH5m .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lONSkC9XDFVUkH5m .icon-shape p,#mermaid-svg-lONSkC9XDFVUkH5m .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lONSkC9XDFVUkH5m .icon-shape .label rect,#mermaid-svg-lONSkC9XDFVUkH5m .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lONSkC9XDFVUkH5m .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lONSkC9XDFVUkH5m .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lONSkC9XDFVUkH5m :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Host App
Plugin SDK / Contract
Plugin A
Plugin B
Plugin Loader
5.2 Android 插件化需要解决什么
类加载
宿主需要通过 DexClassLoader、PathClassLoader 或受控加载机制加载插件代码,并处理:
- 父优先还是子优先;
- 宿主与插件重复类;
- SDK 接口由哪个 ClassLoader 加载;
- 混淆后入口名称如何保持;
- native library 如何加载。
资源加载
插件的 R、主题、布局、图片和语言资源可能与宿主冲突,需要建立独立或合并的资源访问策略。Android 版本变化会提高非官方资源方案的兼容成本。
组件生命周期
未安装 APK 中的 Activity、Service、Receiver 不在宿主 Manifest 内。传统方案可能使用占位组件、代理生命周期或 Hook 系统调用,这会显著增加平台兼容和审核风险。
版本协议
宿主 SDK 与插件必须协商:
kotlin
interface HostPlugin {
val apiVersion: Int
fun install(context: PluginContext)
fun createEntry(): PluginEntry
}
需要定义最低/最高版本、能力发现、向后兼容和失败降级。
安全
动态代码必须验证来源、签名和完整性,并限制权限、文件、网络和宿主 API 访问。插件与宿主通常处于同一进程和权限域,普通 ClassLoader 并不是安全沙箱。
5.3 插件化与热修复
二者都会涉及动态代码,但目标不同:
- 插件化:长期扩展新能力;
- 热修复:临时修复已发布版本中的缺陷。
两者都受到 Android 平台行为、应用商店政策、安全和可维护性的强约束。不能因为"不想发版"就默认选择动态加载代码。
5.4 什么时候应该插件化
只有存在明确运行时扩展需求时,例如:
- 企业内部宿主需要加载不同客户的受控业务包;
- 一个平台产品提供经过审核的扩展生态;
- 大型设备端系统有明确的离线插件协议;
- 插件必须独立于宿主版本按协议演进。
如果目标只是并行开发、编译加速或业务解耦,模块化/组件化已经足够。
6. 三者的核心区别
| 维度 | 模块化 | 组件化 | 插件化 |
|---|---|---|---|
| 主要阶段 | 编译期 | 设计期 + 编译期 + 组装期 | 运行期 |
| 核心边界 | Gradle 编译单元 | 业务能力与公开契约 | 宿主与动态扩展协议 |
| 是否依赖 Gradle Module | 通常是 | 通常基于 Module | 插件可在独立工程构建 |
| 是否独立发布 | 可选 | 可选 | 通常需要 |
| 是否运行时加载 | 否 | 通常否 | 是 |
| 是否要求业务自治 | 不一定 | 是 | 是,并需协议稳定 |
| 隔离强度 | 中 | 中到高 | 高,但同进程未必安全隔离 |
| 主要收益 | 编译、可见性、代码组织 | 团队协作、变化隔离、复用 | 动态扩展、独立交付 |
| 主要成本 | 模块配置与依赖图 | 契约、组装、治理和测试 | 兼容、安全、加载、资源、生命周期 |
一句话概括:
text
模块化 = 把代码拆成编译单元
组件化 = 把能力拆成可通过契约组装的自治单元
插件化 = 把扩展拆成运行时可发现和加载的单元
7. "为了组件化而组件化"的典型症状
7.1 按页面机械建 Module
text
:feature:login-page
:feature:register-page
:feature:forgot-password-page
:feature:sms-code-page
这些页面共享同一账号领域、状态和发布节奏,却被拆成许多小模块。结果是配置文件比业务代码多,跨页面状态变成远程调用式接口。
更合理的边界可能是一个 :feature:account。
7.2 每个 Module 都拆 api 和 impl
api/impl 能隐藏实现,但模块数量会翻倍:
text
:feature:order:api
:feature:order:impl
:feature:payment:api
:feature:payment:impl
如果没有多实现、跨 App 复用、严格编译隔离或独立发布需求,Kotlin internal 和单 Module 的公开包已经可能足够。
7.3 万物进入 common
text
:common
BaseActivity
UserManager
HttpClient
OrderDto
PaymentUtils
GlobalEventBus
common 最终变成所有模块都依赖的"新单体"。任何修改触发大面积重编译,业务边界也全部泄漏。
更好的 Core Module 应围绕稳定能力命名:
text
:core:network
:core:database
:core:design-system
:core:analytics
:core:testing
7.4 为解耦引入全局路由和反射
把类型安全调用改成字符串路由,不一定更解耦:
kotlin
router.open("app://order/detail?id=$id")
如果没有编译期校验,路径、参数、权限和返回值错误只能在运行时暴露。进程内调用优先使用类型安全契约;URI 路由用于真正需要跨模块导航、外部 Deep Link 或跨进程入口。
7.5 使用全局 EventBus 作为组件通信总线
发布者和订阅者表面不互相引用,但真实依赖被隐藏:
- 无法发现谁消费事件;
- 事件顺序和生命周期难以保证;
- 重构没有编译器保护;
- 业务状态出现多个事实来源。
组件通信应优先使用显式接口、导航结果、共享 Repository 或领域数据源。
7.6 Module 数增加,构建反而更慢
过多小 Module 会增加:
- Gradle 配置时间;
- Kotlin/Java 编译任务数量;
- KSP/KAPT 和资源处理任务;
- CI 调度与缓存复杂度;
- IDE 同步和索引成本。
模块化只有在依赖图合理、缓存命中、ABI 稳定且并行度合适时才可能改善构建。应以 Build Scan 和 CI 数据验证,而不是凭感觉。
7.7 组件可以独立运行,但不能在主工程稳定集成
独立 Demo 中使用 Mock 数据运行成功,不代表真实导航、登录态、主题、埋点和异常恢复正确。组件级测试不能替代 App 组装后的集成测试。
8. 过度组件化的真实成本
8.1 认知成本
开发一个简单需求,需要在 api、impl、domain、data、navigation、di 六个模块之间跳转。边界没有减少复杂度,只是把它分散到更多位置。
8.2 抽象成本
为每个类创建接口、DTO、Mapper、Facade 和 Factory,会产生大量只转发调用的代码。抽象应该保护变化边界,而不是满足目录模板。
8.3 版本成本
内部组件全部制品化后,需要处理版本对齐、快照版本、回滚、兼容矩阵和本地联调。单仓库项目使用源码依赖通常更简单。
8.4 调试成本
调用通过路由、反射、服务注册表或 IPC 后,堆栈和数据流更难追踪。动态能力越强,静态分析能力越弱。
8.5 一致性成本
多个团队可能各自选择网络、序列化、DI、状态管理和日志方式。组件自治必须有平台规范和少量稳定 Core 能力支撑,否则会变成技术栈碎片化。
9. 什么情况下值得拆 Module
不要用代码行数作为唯一标准。满足越多条件,拆分价值越高:
| 判断问题 | 是时说明 |
|---|---|
| 是否有独立业务语义? | 边界更可能稳定 |
| 是否由独立团队或负责人维护? | 所有权明确 |
| 是否有不同变化/发布节奏? | 隔离能减少影响 |
| 是否会被多个 App 或 Feature 复用? | 复用收益存在 |
| 是否需要限制源码可见性? | 编译边界有价值 |
| 是否能减少大量无关重编译? | 构建收益可测量 |
| 是否能独立测试? | 输入输出边界清晰 |
| 是否能画出小而稳定的公开 API? | 契约成熟 |
如果唯一理由是"文件太多看着不舒服",先整理 package,而不是立即建 Module。
10. 推荐的分层与组件颗粒度
对中大型 Android 应用,一个稳健的起点是:
text
app/
consumer/ # 产品壳与 Composition Root
feature/
account/ # 按业务能力,而不是按页面
catalog/
cart/
order/
payment/
core/
model/ # 少量跨功能稳定模型
network/ # HTTP 基础设施,不含业务 API
database/ # DB 基础设施,不含所有业务 DAO
designsystem/ # Token、主题、通用组件
navigation/ # 路由基础协议
analytics/ # 埋点端口与基础实现
testing/ # Fake、测试 Rule、Fixture
build-logic/ # Convention Plugins
10.1 Feature 内部结构
简单 Feature 使用单 Module:
text
:feature:cart
src/main/kotlin/.../cart/
api/ # 少量公开入口
presentation/
domain/
data/
di/
复杂且需要强隔离的 Feature 再拆:
text
:feature:order:contract
:feature:order:domain
:feature:order:data
:feature:order:presentation
拆分依据是依赖和团队边界,不是所有 Feature 套同一模板。
10.2 Core 应该放什么
Core 只放跨多个业务、语义稳定、与具体业务无关的能力。
适合:
- 设计系统;
- 网络客户端配置;
- 通用数据库框架;
- 日志、监控和埋点基础设施;
- 测试工具;
- 经过证明的通用值对象。
不适合:
- 只被一个业务使用的 Utils;
- 所有业务 DTO;
- 全局可变 UserManager;
- 为避免循环依赖临时搬来的类;
- 没有清晰所有权的 Base 类。
11. 最佳依赖方向
11.1 基准依赖图
#mermaid-svg-1EJEBk7bYT6mVwKu{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-1EJEBk7bYT6mVwKu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1EJEBk7bYT6mVwKu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1EJEBk7bYT6mVwKu .error-icon{fill:#552222;}#mermaid-svg-1EJEBk7bYT6mVwKu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1EJEBk7bYT6mVwKu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1EJEBk7bYT6mVwKu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1EJEBk7bYT6mVwKu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1EJEBk7bYT6mVwKu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1EJEBk7bYT6mVwKu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1EJEBk7bYT6mVwKu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1EJEBk7bYT6mVwKu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1EJEBk7bYT6mVwKu .marker.cross{stroke:#333333;}#mermaid-svg-1EJEBk7bYT6mVwKu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1EJEBk7bYT6mVwKu p{margin:0;}#mermaid-svg-1EJEBk7bYT6mVwKu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1EJEBk7bYT6mVwKu .cluster-label text{fill:#333;}#mermaid-svg-1EJEBk7bYT6mVwKu .cluster-label span{color:#333;}#mermaid-svg-1EJEBk7bYT6mVwKu .cluster-label span p{background-color:transparent;}#mermaid-svg-1EJEBk7bYT6mVwKu .label text,#mermaid-svg-1EJEBk7bYT6mVwKu span{fill:#333;color:#333;}#mermaid-svg-1EJEBk7bYT6mVwKu .node rect,#mermaid-svg-1EJEBk7bYT6mVwKu .node circle,#mermaid-svg-1EJEBk7bYT6mVwKu .node ellipse,#mermaid-svg-1EJEBk7bYT6mVwKu .node polygon,#mermaid-svg-1EJEBk7bYT6mVwKu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1EJEBk7bYT6mVwKu .rough-node .label text,#mermaid-svg-1EJEBk7bYT6mVwKu .node .label text,#mermaid-svg-1EJEBk7bYT6mVwKu .image-shape .label,#mermaid-svg-1EJEBk7bYT6mVwKu .icon-shape .label{text-anchor:middle;}#mermaid-svg-1EJEBk7bYT6mVwKu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1EJEBk7bYT6mVwKu .rough-node .label,#mermaid-svg-1EJEBk7bYT6mVwKu .node .label,#mermaid-svg-1EJEBk7bYT6mVwKu .image-shape .label,#mermaid-svg-1EJEBk7bYT6mVwKu .icon-shape .label{text-align:center;}#mermaid-svg-1EJEBk7bYT6mVwKu .node.clickable{cursor:pointer;}#mermaid-svg-1EJEBk7bYT6mVwKu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1EJEBk7bYT6mVwKu .arrowheadPath{fill:#333333;}#mermaid-svg-1EJEBk7bYT6mVwKu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1EJEBk7bYT6mVwKu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1EJEBk7bYT6mVwKu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1EJEBk7bYT6mVwKu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1EJEBk7bYT6mVwKu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1EJEBk7bYT6mVwKu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1EJEBk7bYT6mVwKu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1EJEBk7bYT6mVwKu .cluster text{fill:#333;}#mermaid-svg-1EJEBk7bYT6mVwKu .cluster span{color:#333;}#mermaid-svg-1EJEBk7bYT6mVwKu 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-1EJEBk7bYT6mVwKu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1EJEBk7bYT6mVwKu rect.text{fill:none;stroke-width:0;}#mermaid-svg-1EJEBk7bYT6mVwKu .icon-shape,#mermaid-svg-1EJEBk7bYT6mVwKu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1EJEBk7bYT6mVwKu .icon-shape p,#mermaid-svg-1EJEBk7bYT6mVwKu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1EJEBk7bYT6mVwKu .icon-shape .label rect,#mermaid-svg-1EJEBk7bYT6mVwKu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1EJEBk7bYT6mVwKu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1EJEBk7bYT6mVwKu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1EJEBk7bYT6mVwKu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} App / Composition Root
Feature: Order
Feature: Payment
Feature Contracts
Domain Ports & Models
Stable Core Capabilities
Network / DB Implementations
关键原则:
- App 壳知道具体实现并负责组装;
- Feature 之间尽量不直接依赖实现;
- 业务规则依赖抽象端口;
- Data/Infrastructure 实现端口;
- Core 不能反向依赖 Feature。
11.2 避免横向 Feature 依赖链
危险结构:
text
home -> order -> account -> settings -> app-common
更好的方式是提取真正稳定的契约或由 App 层编排:
text
home ----> order-contract <---- order-impl
app ----> home + order-impl
但不要为任何调用都建 Contract Module。只有跨边界能力稳定且值得隐藏实现时才提取。
11.3 循环依赖说明边界有问题
text
:feature:order -> :feature:payment
:feature:payment -> :feature:order
不要用反射或接口注册表把编译循环藏起来。先分析:
- 是否其实属于同一个业务组件;
- 是否应提取稳定领域契约;
- 是否应由上层 App/Coordinator 编排;
- 是否通过共享事实数据而不是相互命令协作。
12. 组件间通信的最佳选择
12.1 类型安全导航契约
kotlin
@Serializable
data class OrderDetailRoute(val orderId: String)
interface OrderNavigator {
fun openOrderDetail(orderId: String)
}
调用方依赖路由契约,App 层实现具体导航。参数应是 ID、枚举和值对象,不要跨组件传递大型可变对象、Activity 或 ViewModel。
12.2 公开 Use Case 或 Facade
kotlin
interface CartComponent {
val itemCount: StateFlow<Int>
suspend fun addItem(productId: ProductId, quantity: Int): AddCartResult
}
Facade 适合暴露少量稳定能力。不要把组件所有内部类都重新包装一遍。
12.3 共享 Repository 作为事实来源
订单创建后,订单列表不一定需要接收"刷新事件"。两个 Feature 可以观察同一个订单数据源:
text
Payment 写入订单状态
↓
OrderRepository 更新事实
↓
Order List 观察到新状态
这比全局 EventBus 更容易恢复,也更符合单一事实来源。
12.4 领域事件
当多个业务确实需要响应已经发生的领域事实时,可以定义有语义、可追踪的领域事件:
kotlin
sealed interface OrderEvent {
data class Paid(
val orderId: OrderId,
val paidAt: Instant,
) : OrderEvent
}
需要明确事件的持久化、重放、顺序、幂等和失败策略。进程内 SharedFlow 不等价于可靠事件总线。
12.5 IPC 只用于真正的进程边界
AIDL、Binder、ContentProvider 适合跨进程或外部应用契约。为了"看起来解耦"在同一进程内引入 IPC,会带来序列化、线程、安全和异常恢复成本。
13. 依赖注入与组件组装
组件不应该自行从全局 Service Locator 偷取依赖。最终产品壳应作为 Composition Root:
kotlin
class AppContainer(
api: ShopApi,
database: ShopDatabase,
) {
private val orderRepository: OrderRepository =
DefaultOrderRepository(api, database.orderDao())
val orderComponent: OrderComponent =
DefaultOrderComponent(orderRepository)
}
使用 Hilt/Dagger/Koin 时原则相同:
- 领域层不依赖 DI 框架;
- App 壳绑定接口和实现;
- Feature 只声明它需要的依赖;
- 避免任何模块都能注入任何全局对象。
对外公开组件 Factory 也是清晰方案:
kotlin
interface PaymentComponentFactory {
fun create(
orderId: OrderId,
result: PaymentResultHandler,
): PaymentComponent
}
Factory 的参数就是组件的显式输入边界。
14. Gradle 最佳实践
14.1 使用 Convention Plugin 统一配置
不要在几十个 build.gradle.kts 中复制 Android、Kotlin、Compose 配置:
text
build-logic/
convention/
AndroidApplicationConventionPlugin.kt
AndroidLibraryConventionPlugin.kt
AndroidComposeConventionPlugin.kt
KotlinLibraryConventionPlugin.kt
Feature 配置可以缩小为:
kotlin
plugins {
id("shop.android.feature")
id("shop.android.compose")
}
dependencies {
implementation(projects.core.designsystem)
implementation(projects.feature.order.domain)
}
14.2 使用 Version Catalog
集中管理第三方依赖别名和兼容版本,避免各组件各自声明版本。Version Catalog 解决声明一致性,不替代依赖升级策略和兼容验证。
14.3 缩小公开 ABI
- 默认使用
internal; - 限制
public类型数量; - 避免公开 API 暴露 Retrofit、Room、Compose 内部实现类型;
- 优先
implementation; - 对稳定契约做兼容性检查。
14.4 验证依赖规则
仅写架构文档不够,应自动阻止错误依赖:
- Gradle 配置检查;
- 自定义 lint/Detekt 规则;
- Module Graph 验证;
- API/二进制兼容性检查;
- CI 中禁止 Feature 依赖其他 Feature 的实现模块。
14.5 用数据评估构建收益
记录并比较:
- IDE 同步时间;
- clean build;
- 修改一个 Feature 后的增量构建;
- Kotlin 编译和 KSP/KAPT 时间;
- Remote Build Cache 命中率;
- CI 关键路径。
没有基线数据,就无法判断拆分是否真的改善构建。
15. 推荐的渐进式实施方案
最佳方案不是一次性"大爆炸"重构,而是按风险和收益递进。
阶段一:先整理单体
目标:验证业务边界。
- 改为 package by feature;
- 把业务规则从 Activity/Fragment/ViewModel 中抽离;
- 使用
internal和少量接口控制可见性; - 清理无归属的
common和全局状态; - 画出当前真实依赖图。
如果 package 边界都无法保持,拆成 Module 只会把混乱搬到 Gradle。
阶段二:提取稳定 Core
优先提取被广泛复用、语义稳定的基础能力:
text
:core:design-system
:core:network
:core:testing
不要先提取充满业务类型的 common-utils。
阶段三:提取高价值 Feature
选择一个满足以下条件的 Feature 试点:
- 边界清晰;
- 变更频繁;
- 有明确负责人;
- 测试相对完整;
- 与其他业务的入口数量有限。
记录拆分前后的构建、缺陷和协作数据。
阶段四:建立契约和组装层
只有当跨 Feature 依赖真实存在时,再建立:
- 类型安全路由;
- Component Facade;
- Domain Port;
- App Composition Root;
- 依赖方向的自动检查。
阶段五:按需要细分
当单个 Feature 本身由多人维护、需要多 App 复用或存在不同平台实现时,再拆 contract/domain/data/presentation。
阶段六:最后评估动态交付或插件化
只有 APK 体积、安装转化、客户扩展或独立交付数据证明有必要时,才引入 Dynamic Feature 或插件系统。
#mermaid-svg-mD9QQKTtSLCKqOIG{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-mD9QQKTtSLCKqOIG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mD9QQKTtSLCKqOIG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mD9QQKTtSLCKqOIG .error-icon{fill:#552222;}#mermaid-svg-mD9QQKTtSLCKqOIG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mD9QQKTtSLCKqOIG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mD9QQKTtSLCKqOIG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mD9QQKTtSLCKqOIG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mD9QQKTtSLCKqOIG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mD9QQKTtSLCKqOIG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mD9QQKTtSLCKqOIG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mD9QQKTtSLCKqOIG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mD9QQKTtSLCKqOIG .marker.cross{stroke:#333333;}#mermaid-svg-mD9QQKTtSLCKqOIG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mD9QQKTtSLCKqOIG p{margin:0;}#mermaid-svg-mD9QQKTtSLCKqOIG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mD9QQKTtSLCKqOIG .cluster-label text{fill:#333;}#mermaid-svg-mD9QQKTtSLCKqOIG .cluster-label span{color:#333;}#mermaid-svg-mD9QQKTtSLCKqOIG .cluster-label span p{background-color:transparent;}#mermaid-svg-mD9QQKTtSLCKqOIG .label text,#mermaid-svg-mD9QQKTtSLCKqOIG span{fill:#333;color:#333;}#mermaid-svg-mD9QQKTtSLCKqOIG .node rect,#mermaid-svg-mD9QQKTtSLCKqOIG .node circle,#mermaid-svg-mD9QQKTtSLCKqOIG .node ellipse,#mermaid-svg-mD9QQKTtSLCKqOIG .node polygon,#mermaid-svg-mD9QQKTtSLCKqOIG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mD9QQKTtSLCKqOIG .rough-node .label text,#mermaid-svg-mD9QQKTtSLCKqOIG .node .label text,#mermaid-svg-mD9QQKTtSLCKqOIG .image-shape .label,#mermaid-svg-mD9QQKTtSLCKqOIG .icon-shape .label{text-anchor:middle;}#mermaid-svg-mD9QQKTtSLCKqOIG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mD9QQKTtSLCKqOIG .rough-node .label,#mermaid-svg-mD9QQKTtSLCKqOIG .node .label,#mermaid-svg-mD9QQKTtSLCKqOIG .image-shape .label,#mermaid-svg-mD9QQKTtSLCKqOIG .icon-shape .label{text-align:center;}#mermaid-svg-mD9QQKTtSLCKqOIG .node.clickable{cursor:pointer;}#mermaid-svg-mD9QQKTtSLCKqOIG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mD9QQKTtSLCKqOIG .arrowheadPath{fill:#333333;}#mermaid-svg-mD9QQKTtSLCKqOIG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mD9QQKTtSLCKqOIG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mD9QQKTtSLCKqOIG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mD9QQKTtSLCKqOIG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mD9QQKTtSLCKqOIG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mD9QQKTtSLCKqOIG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mD9QQKTtSLCKqOIG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mD9QQKTtSLCKqOIG .cluster text{fill:#333;}#mermaid-svg-mD9QQKTtSLCKqOIG .cluster span{color:#333;}#mermaid-svg-mD9QQKTtSLCKqOIG 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-mD9QQKTtSLCKqOIG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mD9QQKTtSLCKqOIG rect.text{fill:none;stroke-width:0;}#mermaid-svg-mD9QQKTtSLCKqOIG .icon-shape,#mermaid-svg-mD9QQKTtSLCKqOIG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mD9QQKTtSLCKqOIG .icon-shape p,#mermaid-svg-mD9QQKTtSLCKqOIG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mD9QQKTtSLCKqOIG .icon-shape .label rect,#mermaid-svg-mD9QQKTtSLCKqOIG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mD9QQKTtSLCKqOIG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mD9QQKTtSLCKqOIG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mD9QQKTtSLCKqOIG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是且收益明确
Package by Feature
Stable Core Modules
High-value Feature Modules
Contracts & Composition Root
Selective Fine-grained Split
需要运行时扩展?
保持组件化
Dynamic Delivery / Plugin
16. 一套推荐的基准架构
对于 10---50 人、单仓库、一个或多个 Android App 的团队,可以采用以下默认方案:
16.1 组织方式
- 按业务 Feature 划分一级边界;
- 每个 Feature 默认一个 Android Library Module;
- Feature 内按
presentation/domain/data使用 package 分层; - 只有高复杂度 Feature 才进一步拆 Module;
- App 壳负责导航和依赖组装;
- Core 按稳定技术能力拆分;
- Build Logic 统一 Gradle 配置。
16.2 依赖规则
text
app -> feature implementations
feature -> its domain + stable core
data implementation -> domain port
feature A -X-> feature B implementation
core -X-> feature
domain -X-> Android UI / Retrofit / Room
16.3 通信规则
- 导航使用类型安全 Route/Contract;
- 跨业务查询使用公开 Use Case/Facade;
- 共享事实使用 Repository;
- 领域事件只表达已发生事实;
- 禁止使用全局 EventBus 代替契约;
- 外部 Deep Link 在 App 层解析后转换成内部类型。
16.4 发布规则
- 单仓库内部模块优先源码依赖;
- 真正跨仓库复用的稳定 Library 才制品化;
- Dynamic Feature 只解决按需交付;
- 插件化必须有明确产品需求、安全模型和兼容协议。
这套方案不是唯一答案,但它把默认成本控制在合理范围,同时保留继续演进的空间。
17. 如何判断组件化是否成功
不要用 Module 数量做 KPI。更有意义的指标是:
17.1 变化隔离
- 修改支付实现是否会迫使订单 UI 重编译;
- 网络 DTO 变化是否泄漏到其他 Feature;
- 一个组件能否在不修改调用者的情况下重构内部实现。
17.2 构建效率
- 常见修改的增量构建是否更快;
- CI 是否只运行受影响测试;
- 缓存命中率是否提高;
- 构建关键路径是否缩短。
17.3 团队协作
- 代码所有权是否明确;
- 跨团队需求是否通过稳定契约协作;
- 合并冲突和发布协调是否减少;
- 新成员能否快速定位一个需求的修改范围。
17.4 质量
- 组件级测试是否容易编写;
- 循环依赖是否被自动阻止;
- 公共 API 是否保持小而稳定;
- 集成缺陷是否下降。
如果模块数翻倍,而这些指标没有改善,说明拆分策略需要回退或合并。
18. 什么时候应该合并组件
架构允许演进,也应该允许合并。出现以下情况时可以考虑合并:
- 两个模块总是同时修改、同时发布;
- 一个模块只有几行转发代码;
- 模块之间接口数量持续膨胀;
- 为避免循环依赖创建了大量中间模块;
- 构建配置成本高于隔离收益;
- 团队所有权已经变化;
- 原业务边界被产品演进证明不成立。
合并不是架构失败,而是根据新证据调整边界。
19. 常见问题
一个 Feature 应该包含几个页面?
没有固定数字。围绕同一业务能力、共享状态和同一变化节奏的页面可以属于一个 Feature。不要按 Activity/Fragment 数量拆分。
Domain 是否必须单独一个 Module?
不必须。只有当需要强制纯 Kotlin、跨平台复用、多人并行或限制依赖时,独立 Domain Module 才有明显价值。
Feature 能否直接依赖另一个 Feature?
偶尔可以,但应依赖稳定契约而非实现。大量横向依赖通常意味着边界或编排层设计有问题。
组件化一定能提升编译速度吗?
不一定。收益取决于依赖图、ABI、注解处理、缓存和实际修改模式。必须测量。
是否应该把所有组件发布成 AAR?
不应该。单仓库内部模块通常使用源码依赖更容易调试和重构。跨仓库、跨团队、版本独立时再制品化。
Compose 会改变组件化方案吗?
不会改变业务边界原则,但 Compose 让 UI Contract 更容易表达为 @Composable 入口、状态与事件。不要让 Feature 对外暴露内部 ViewModel;优先暴露 Route、Screen 参数或 Component Factory。
插件化能绕过应用商店发版吗?
不能把它当作默认目标。动态代码受到平台安全、兼容性和分发政策约束,应根据实际分发环境和政策进行评估。
20. 最终结论
- 模块化提供编译边界,但不自动带来业务解耦;
- 组件化以业务能力为核心,通过契约、依赖规则和组装实现自治;
- 插件化解决运行时扩展,复杂度和风险远高于普通组件化;
- 颗粒度越细不代表架构越好,每一个边界都应该有可验证的收益;
- 小项目先用 package by feature,中大型项目按稳定业务边界渐进拆 Module;
api/impl、独立运行、AAR 发布、Dynamic Feature 和插件化都应该按需要引入;- App 壳负责组装,Feature 依赖稳定 Core 与领域契约,禁止横向穿透实现;
- 用构建时间、变化隔离、团队协作和缺陷率衡量结果,而不是统计 Module 数量。
最稳妥的最佳实践可以浓缩为一句话:
从业务边界出发,先用最低成本验证边界,再逐步增强编译、依赖、交付和运行时隔离;只为已经发生的复杂度付费。