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

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

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

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

类加载

宿主需要通过 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. 推荐的渐进式实施方案

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

阶段一:先整理单体

目标:验证业务边界。

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

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

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

相关推荐
聚美智数7 小时前
智能结构化识别-图片结构解析-图像结构化解析-照片结构化解析
android·java·数据库
mmsx12 小时前
Android 插件化架构:40 个 Gradle 模块的拆分、通信与共存
android·kotlin
mmsx12 小时前
Android 不改一行源码:用 AspectJ 做三类拦截
android·kotlin
TST-魚爷12 小时前
易视讯会议摄像头 + 全向麦克风实操讲解稿
android·电脑·钉钉·腾讯会议·zoom会议
嘟哩DuliDuli12 小时前
AI创作工作区的项目上下文与多模态任务边界
android·人工智能·安全·ai·软件工程
imDwAaY14 小时前
MySQL 存储引擎是什么?
android·数据库·mysql
熊猫钓鱼>_>15 小时前
开源鸿蒙平台 KMP 三方库 kotlin-csv 适配全流程:从编码假设到沙箱真机互通验证
开发语言·人工智能·华为·kotlin·harmonyos·鸿蒙
Meteors.15 小时前
Android Compose学习02:基础组件与 Modifier
android·学习
其实防守也摸鱼16 小时前
存储管理:页面置换算法学习任务(测验题要求给出置换页面顺序,缺页次数,缺页率)
android·前端·数据库·学习·oracle·自动化
mmsx17 小时前
Android 地图上绘制 KML:解析、样式与渲染
android·xml