Android组件化指南

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 implementationapi

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 定义

组件化是在模块化基础上,以业务能力或稳定基础能力为边界,通过公开契约、依赖规则和组装机制形成可组合单元。

一个真正的组件至少应回答:

  1. 对外提供什么能力?
  2. 需要外部提供什么依赖?
  3. 哪些类型是公开契约?
  4. 内部实现能否在不影响调用者的情况下变化?
  5. 谁拥有它,如何测试和发布?

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 插件化需要解决什么

类加载

宿主需要通过 DexClassLoaderPathClassLoader 或受控加载机制加载插件代码,并处理:

  • 父优先还是子优先;
  • 宿主与插件重复类;
  • 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 都拆 apiimpl

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 认知成本

开发一个简单需求,需要在 apiimpldomaindatanavigationdi 六个模块之间跳转。边界没有减少复杂度,只是把它分散到更多位置。

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. 推荐的渐进式实施方案

最佳方案不是一次性"大爆炸"重构,而是按风险和收益递进。

阶段一:先整理单体

目标:验证业务边界。

  1. 改为 package by feature;
  2. 把业务规则从 Activity/Fragment/ViewModel 中抽离;
  3. 使用 internal 和少量接口控制可见性;
  4. 清理无归属的 common 和全局状态;
  5. 画出当前真实依赖图。

如果 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 数量。

最稳妥的最佳实践可以浓缩为一句话:

从业务边界出发,先用最低成本验证边界,再逐步增强编译、依赖、交付和运行时隔离;只为已经发生的复杂度付费。

相关推荐
binbin_521 小时前
HarmonyOS 应用功耗优化实战:定位耗电、收口任务与验证回归
android·回归·harmonyos
AFinalStone2 小时前
Android 7系统无障碍服务(七)手势分发与 KeyEvent 处理
android·无障碍服务
事圆则缓3 小时前
Jetpack Compose Effect 完全指南
android·kotlin
平头哥技术团队12 小时前
Day 10 | 工欲善其事:VS Code 配置与项目归档
android·开发语言·前端·javascript·html·交互
冬木家居13 小时前
40㎡客厅变形记,小家住出大自由[特殊字符]
android·经验分享·笔记·智能家居·微信公众平台
AFinalStone13 小时前
Android 7系统无障碍服务(六)输入事件拦截与 TouchExplorer
android·无障碍服务
天空之城--13 小时前
MT管理器Android逆向工程完全指南:从入门到实战
android
Anhty15 小时前
2026九月最新变声器测评:iOS安卓双端适配,低延迟运行更稳定
android·人工智能·功能测试·ios·智能手机
渡我白衣16 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集