Android 组件化概念和特征

Android 组件化概念和特征


先说个故事:为什么需要组件化?

假设你接手了一个"祖传" App,代码全塞在一个 module 里,3 年没人敢动。某产品经理说:"把书签功能拆出来,给另一个 App 用。"

你打开项目一看:

复制代码
所有代码都在 app/ 里
用户模块、订单模块、支付模块、网络模块全部混在一起
改一行代码,编译 5 分钟
git blame 查一个人,文件名都找不到

组件化就是来解决这个痛苦的。


一、先搞懂两个概念:模块化 vs 组件化

这是很多教程含糊的地方,先说清楚:

模块化(Modularization) 组件化(Componentization)
官方名称 官方叫 Modularization 业务层面的组件化约定
本质 代码组织结构 可独立交付的业务单元
核心 把代码拆成多个 Gradle 模块 在模块内部再拆 api + impl
解决的问题 编译慢、边界不清、多人协作困难 业务功能之间不互相依赖界面
依赖方式 直接 project(':module') 引用 通过接口/路由调用,不直接引用
运行方式 只能作为整体 APP 运行 可以单独运行,也可以集成运行
发布方式 源码在同一仓库 可发布到远程仓库,作为 AAR/SDK 被其他项目引用
封装边界 模块内部可以互相访问 impl 完全隐藏,只暴露 api/facade
类比 把一栋楼分成商铺、水电、网络 每个商铺只挂招牌(api),不让你进后厨(impl)

记住:模块化是"物理拆分",组件化是"逻辑边界规则"。模块化是"把代码拆开",组件化是"拆开后每个零件还能单独卖"。NIA 项目两者都用上了。


二、真正的组件化是什么样的?

用一个实际例子说明,假设你要做一个「图片选择器」组件:

模块化的做法

复制代码
app/
 └── module_image_picker/   ← 直接引用,直接依赖
      ├── ImagePickerActivity.kt
      ├── ImagePickerViewModel.kt
      └── ImagePickerAdapter.kt

其他模块直接调用:

kotlin 复制代码
// ❌ 直接 import,直接依赖
startActivity(Intent(this, ImagePickerActivity::class.java))
// 或者
implementation project(':module_image_picker')

问题:

  • module_image_picker 的界面代码暴露给所有人
  • 改一下 Activity 名字,所有引用的地方都要改
  • 想把这个功能给另一个 App 用?源码级别引用,耦合严重
  • 组件内部实现所有人都能看到,无法封装

三、组件化的做法

复制代码
image_picker/
 ├── image_picker_api/       ← Facade(只暴露门牌号)
 │    ├── ImagePickerService.kt   ← 接口定义
 │    ├── ImagePickerNavKey.kt    ← 路由 Key
 │    └── Router Constants        ← 路由 Path 常量
 │
 └── image_picker_impl/        ← 完整实现(隐藏)
      ├── ImagePickerActivity.kt
      ├── ImagePickerViewModel.kt
      └── adapter/

关键区别 1:impl 对外不可见

groovy 复制代码
// image_picker_impl 的 build.gradle
// 对外只发布 api,impl 是 internal 实现细节
// 其他 module 只能依赖 image_picker_api,不能依赖 image_picker_impl

关键区别 2:通过 Service 接口调用

kotlin 复制代码
// 其他组件调用时(只看得到 api)
val picker = ARouter.getInstance()
    .build("/image_picker/service")
    .navigation(ImagePickerService::class.java)

picker?.launchImagePicker(
    context = this,
    maxSelect = 9,
    onResult = { list -> /* 处理结果 */ }
)

关键区别 3:可单独运行

groovy 复制代码
# gradle.properties
image_picker.enable_component = true
groovy 复制代码
# image_picker_impl/build.gradle
if (image_picker.enable_component.toBoolean()) {
    apply plugin: 'com.android.application'  // 变成独立 APP
} else {
    apply plugin: 'com.android.library'        // 作为 AAR 发布
}

关键区别 4:可发布到远程仓库

groovy 复制代码
# image_picker_impl/build.gradle
publishing {
    publications {
        release(MavenPublication) {
            artifact("$buildDir/outputs/aar/image_picker_impl-release.aar")
            groupId = "com.example.components"
            artifactId = "image-picker"
            version = "1.3.2"
        }
    }
}

其他项目引用方式:

groovy 复制代码
# 另一个项目的 build.gradle
dependencies {
    implementation 'com.example.components:image-picker:1.3.2'
}

另一个项目根本不需要知道 image_picker 内部有多少个类、多少个 Activity,只通过 api 提供的接口调用,这才是组件化 vs 模块化的本质区别。


四、组件化的三大本质特征

特征 1:可独立交付

复制代码
模块化:源码在同一仓库,编译时合并
组件化:发布到 Maven/Artifactory,其他项目引用二进制包

对比:

复制代码
模块化:
  ┌─────────────────────────────────────┐
  │           同一个 Project             │
  │  app ──依赖── module_home            │
  │                ↓                     │
  │          源码级别耦合                │
  └─────────────────────────────────────┘

组件化:
  ┌──────────┐     ┌──────────────────┐
  │   App A   │     │      App B        │
  │  (项目A)  │     │    (项目B)        │
  └────┬─────┘     └───────▲─────────┘
       │                     │
       │  implementation     │
       │  'com.xxx:comp:1.0' │
       ↓                     │
  ┌──────────────────────────────────┐
  │         Maven Repository          │
  │    image-picker:1.0.2 (AAR)      │
  │    user-center:2.1.0 (AAR)       │
  └──────────────────────────────────┘

特征 2:严格的单向依赖 + 隐藏 impl

复制代码
模块化依赖:
  app → module_home → module_base
  (可以多层依赖,impl 对上层可见)

组件化依赖:
  组件A (api) ← 组件B (impl)
  组件B (api) ← 组件A (impl)
  (api 层级是单向的,impl 层级完全隔离)

模块化的依赖是透明的 (能 import 就能调用),组件化的依赖是受限的(只能通过 facade 调用)。


特征 3:契约化通信(Router / Service / Event)

复制代码
模块化通信:
  直接调用函数 / 直接 import class
  → 高效但强耦合

组件化通信:
  调用方 ──路由 Path──→ 路由表 ──→ 实现方
  调用方 ──接口──→ ServiceLoader ──→ 实现方
  调用方 ──事件──→ 消息总线 ──→ 订阅方

  → 通过中间层间接调用,解耦

五、真实项目中的组件化举例

举例 1:ARouter 官方 Demo(最小化的组件化)

复制代码
app(壳)
  └── 业务组件
       ├── module_user        ← 可独立运行 + 发布到 Maven
       │    ├── user_api/
       │    │    ├── UserService.kt
       │    │    └── RoutePath.kt
       │    └── user_impl/
       │         ├── LoginActivity.kt
       │         └── UserInfoActivity.kt
       │
       └── module_order       ← 同上

在 module_order 的 build.gradle 里:

groovy 复制代码
// 单独运行时是 application
// 作为组件时是 library
if (project.hasProperty('isRunAlone') && isRunAlone.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

// 发布配置
if (!project.hasProperty('isRunAlone') || !isRunAlone.toBoolean()) {
    publishing {
        publications {
            release(MavenPublication) {
                from components.release
                groupId = 'com.example'
                artifactId = 'module-order'
                version = '1.2.0'
            }
        }
    }
}

独立运行 :命令行加 -PisRunAlone=true 切换模式


举例 2:App 商务差旅平台(组件化实战)

业务场景:公司内部 App,HR、财务、业务三套功能需要复用给不同子公司。

复制代码
maven-repo/                        ← 私有 Maven 仓库
├── com/company/
│    ├── app-core:1.0.3           ← 网络/日志/工具
│    ├── component-auth:2.1.0     ← 登录/注册/SSO
│    ├── component-expense:1.5.2  ← 报销单/审批流
│    └── component-notification:1.0.0  ← 推送/消息中心

项目A(主App,研发部内部用)
  └── 依赖
       ├── app-core:1.0.3
       ├── component-auth:2.1.0
       ├── component-expense:1.5.2
       └── component-notification:1.0.0

项目B(子公司定制App)
  └── 依赖
       ├── app-core:1.0.3
       └── component-auth:2.1.0   ← 只需要登录,不需要报销

组件的 api 契约:

kotlin 复制代码
// component-auth/1.0.0/auth_api/auth.kt
interface AuthComponent {
    fun isLoggedIn(): Boolean
    fun getToken(): String?
    fun logout()
}

// 路由常量
object AuthRoutes {
    const val LOGIN = "/auth/login"
    const val LOGOUT = "/auth/logout"
    const val SERVICE = "/auth/service"  // 供其他组件调用服务
}

实现方:

kotlin 复制代码
// component-auth/2.1.0/auth_impl/AuthServiceImpl.kt
@Route(path = AuthRoutes.SERVICE, name = "认证服务")
class AuthServiceImpl : IProvider, AuthComponent {
    override fun isLoggedIn() = token.isNotEmpty()
    override fun getToken() = preferences.getString("token", null)
    override fun logout() { ... }
}

调用方(完全不知道 AuthServiceImpl 在哪个包):

kotlin 复制代码
// 在项目A的某个业务模块里
val auth = ARouter.getInstance()
    .build(AuthRoutes.SERVICE)
    .navigation(AuthComponent::class.java)

if (auth?.isLoggedIn() == true) {
    // 刷新 token
}

好处:

  • 认证功能改版时,只用发新版本 component-auth:2.2.0,项目 A 更新依赖版本即可
  • 项目 B 不需要升级,因为接口没变
  • 认证团队可以独立发版,不影响业务 App

举例 3:设计系统组件化(最典型的组件化场景)

这是组件化最成功、最普遍的应用场景------Design System 组件化。

复制代码
design-system/                        ← 独立发布的组件
├── design-system-api/                ← 契约
│    ├── Theme.kt                     ← 主题配置
│    ├── ButtonSpec.kt                ← 按钮规格
│    └── DesignTokens.kt              ← 设计 Token
│
└── design-system-impl/               ← 实现(不暴露)
     ├── NiaButton.kt
     ├── NiaCard.kt
     ├── NiaTextField.kt
     └── typography/

发布到 Maven:
  implementation 'com.example:design-system:2.0.0'

其他项目引用:

groovy 复制代码
// AppA/build.gradle
dependencies {
    implementation 'com.example:design-system:2.0.0'
}

// 业务代码只需要知道 api
NiaButton(
    text = "确认",
    onClick = { ... }
)

特点:

  • 组件内部怎么实现,调用方完全不关心
  • 设计团队可以独立更新版本,App 团队按需升级
  • 这就是真正的"组件"------独立交付,开箱即用

六、之前讲的 NIA 项目,其实更接近

模块化 + 组件化混合,因为:

  • ✅ 有 api/impl 分离(组件化特征)
  • ✅ 有单向依赖(组件化特征)
  • ❌ 模块没有发布到远程 Maven(更像是模块化)
  • ❌ 模块没有单独交付给其他 App(更像是模块化)

真正的组件化项目,比如:

  • 美团外卖:组件发布到内部 Maven,团队独立发版
  • ARouter 官方 Demo:组件可发布、可独立运行
  • Flutter 的包生态:每个 pub 包都是组件化

如果你想真正做组件化,关键三步:

  1. 拆出 api 模块,impl 只对内,api 对外暴露契约
  2. 组件能发布到 Maven ,其他项目 implementation 'com.xx:component:1.0' 就能用
  3. 组件能独立运行 (isComponent=true 时是 application),开发阶段不需要启动整个 App

七、组件化如何独立运行?(切换 isComponent)

这是"组件化"名字的原始含义------一个功能可以单独编译成 APK 运行。

通过 Gradle 控制:

groovy 复制代码
// gradle.properties
isComponent = true  // 改为 true 可以单独运行
groovy 复制代码
// feature/foryou/build.gradle.kts
if (isComponent.toBoolean()) {
    apply plugin: 'com.android.application'  // 变成可运行 APP
} else {
    apply plugin: 'com.android.library'      // 变成依赖库
}
groovy 复制代码
// app/build.gradle.kts
dependencies {
    if (!isComponent.toBoolean()) {
        // 集成模式下才依赖;单独运行时不依赖,自己就是 APP
        implementation(projects.feature.foryou)
    }
}

效果:

isComponent foryou module app module
true(组件模式) 编译成独立 APK 不依赖 foryou
false(集成模式) 编译成 AAR 依赖 foryou

八、组件化常见问题

1. 组件独立运行问题(isCanRunAlone)

问题:业务组件拆分后,组件是一个 Library Module,没有 Application 和启动 Activity,无法像独立 App 一样运行和调试。

解法 :在 gradle.properties 中配置开关,通过 Build Variant 切换组件的运行模式:

groovy 复制代码
// gradle.properties
# 组件化开关
isModule=true
# 当前调试的组件名
currentModule=user

组件的 build.gradle 根据开关切换 com.android.application 和 com.android.library:

groovy 复制代码
if (isModule.toBoolean()) {
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

独立运行时需要一个专用的 Application 和启动 Activity(放在 src/main/ 或 src/debug/ 下),集成到主工程时这些文件不参与编译。


2. 组件内无法获取 Application 生命周期

问题:组件化后各组件无 Application 实例,很多第三方 SDK(推送、埋点、图片库)需要在 Application 中初始化,且主工程 Application 的生命周期事件无法传递到业务组件。

解法:

方案 说明
模块化生命周期框架(AppJoint / modular-core) 通过注解在主工程 Application 回调时自动分发到各组件,组件只需声明 @ModuleSpec
ContentProvider 手动初始化 组件创建 ContentProvider 在 onCreate 中做初始化,先于 Application 执行,但顺序不可控
TheRouter 自动初始化任务 通过编译期构建有向无环图,自动感知 Application 生命周期,无需外部调用
启动器框架(App Startup) 利用 ContentProvider 早期加载机制 + 懒加载链,按依赖顺序初始化各组件

3. ARouter 踩坑(最常见的组件化路由问题)

ARouter 常见问题:

  • 编译时常量拼写错误 :路由路径写错(如 /login/success 拼成 /login/seccess)导致 ARouter.getInstance().build() 找不到目标,编译期不报错,运行时页面空白。
  • PathVariable 和参数类型不匹配 :/user/{id} 中 {id} 是 String,但传入 Integer,造成强转崩溃。
  • 拦截器未生效 :全局拦截器注册在 ARouter.init() 之前,导致拦截器逻辑被跳过。
  • 混淆后路由失效 :使用 ARouter 时未在 proguard-rules.pro 中添加对应 keep 规则,混淆后路由表丢失。
  • 模块间跳转白屏 :组件 A 通过 ARouter 跳转到组件 B 的页面,但组件 B 的 Activity 未在主工程 AndroidManifest.xml 中注册(独立调试时注册了,但集成时忘了合并)。

解法:

  • 路由路径统一管理到组件的 Export Module 中,用常量类而非字符串字面量。
  • 路由参数统一使用 @Autowired + 类型推导,避免手动拼接。
  • 混淆规则:
proguard 复制代码
-keep class com.alibaba.android.arouter.** { *; }
-keep class * implements com.alibaba.android.arouter.facade.template.ISyringe { *; }
-keep class * implements com.alibaba.android.arouter.facade.template.IProvider { *; }

4. 资源冲突

问题 :不同组件的布局文件、字符串、drawable 同名,编译时报 resource merge 冲突,后者覆盖前者。

解法:

groovy 复制代码
// 每个组件的 build.gradle
android {
    resourcePrefix "user_"   // 强制该模块所有资源名以 user_ 开头
}

命名空间冲突(namespace 与 applicationId 不一致)也会引发 R.java 生成的路径问题,建议 namespace 与包名保持一致:

groovy 复制代码
android {
    namespace "com.example.module.user"
    resourcePrefix "user_"
}

5. 组件间循环依赖

问题:A 组件需要调用 B 组件的接口,B 组件也需要调用 A 组件的接口,形成循环依赖,Gradle 编译直接失败。

解法:

  • 接口下沉(ServiceLoader) :公共接口放到 module-api 模块,A 和 B 都只依赖 module-api,实现类在各自组件内,通过手动或注解注册到服务池。
  • Export + Implement 双模块 (美团方案):每个业务组件拆成 Export Module(只含接口/路由 path/事件定义)和 Implement Module(具体实现)。A → B 的调用只依赖 B 的 Export,B → A 的调用只依赖 A 的 Export,完全对等的层级关系,消除循环依赖。
  • 避免过度拆分:不是所有接口都要互相调用,优先考虑业务边界是否真的需要双向通信。

6. 组件间通信方案的选型困惑

方案 适用场景 缺陷
直接依赖 临时过渡 高耦合,组件化意义全无
EventBus/RxBus 跨组件异步事件 消息名称无约束、易滥用、难以追溯
广播 系统级事件 权限、生命周期管理复杂
ARouter/WMRouter 页面跳转 + 接口调用 传参受限于可序列化类型
ServiceLoader + 接口 需要返回对象引用、复杂交互 需要额外的注册/发现机制
LiveDataBus 生命周期感知的跨组件事件 同样有 EventBus 的消息名称问题

推荐组合 :路由框架(ARouter/WMRouter/TheRouter)+ 接口服务(ServiceLoader)+ 消息总线(基于 LiveData 的 modular-event)三位一体,覆盖页面跳转、接口调用、跨组件事件三大场景。


7. Application 合并冲突

问题 :每个组件都有一个自定义 Application 继承 android.app.Application,集成到主工程后多个 Application 类合并,编译时报 Application has two definitions。

解法:

  • 组件的 Application 类放在 src/main/ 下,主工程 App Module 依赖该组件时通过 javaCompileOptions 或自定义 Transform 合并(或在主工程 Application 中手动调用组件初始化)。
  • 组件独立运行时使用自己的 Application,集成模式下主工程 Application 充当统一入口,组件通过生命周期框架被动初始化。

8. 组件版本管理失控

问题:组件独立发版后,主工程和各组件依赖的底层库版本不一致(尤其是 Retrofit、OkHttp 等公共库),引发运行时 Method Not Found 或行为不一致。

解法:

groovy 复制代码
// 根目录 build.gradle
ext.versions = [
    retrofit: '2.9.0',
    okhttp: '4.12.0',
    arouter: '1.5.2'
]

// 子模块 build.gradle
implementation "com.squareup.retrofit2:retrofit:${rootProject.ext.versions.retrofit}"

或使用 Gradle 7.0+ 的 version catalog (gradle/libs.versions.toml)统一管理所有组件和依赖版本。


9. 组件内 Fragment 的管理

问题:组件内的 Fragment 需要被主工程或另一个组件宿主使用,但 Fragment 生命周期与宿主 Activity 绑定,直接使用会引发生命周期混乱。

解法:

  • 组件对外暴露的不是 Fragment,而是带路由 Path 的 Activity 或通过接口返回一个 Fragment 实例。
  • 使用 FragmentManager 的 findFragmentById 在运行时动态获取。
  • 组件内 Fragment 的初始化参数通过 Bundle 注入,不直接 new 实例。

总结

问题 核心矛盾 推荐解法
独立运行 Library 无 Application Build Variant + isModule 开关
Application 生命周期 组件感知不到 onCreate 生命周期框架(AppJoint / modular-core / TheRouter)
ARouter 踩坑 路由路径无约束、混淆失效 常量类管理路由 + ProGuard 规则
资源冲突 多模块资源同名 resourcePrefix 前缀 + namespace 隔离
循环依赖 双向调用无法分层 Export+Implement 双模块 或 ServiceLoader
消息总线滥用 事件名称无约束、难追溯 LiveData + @ModuleEvents 强类型注解
Application 合并 多 Module 都有 Application 主工程统一初始化入口
版本冲突 公共库版本不统一 version catalog 集中管理
相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo5 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077005 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work5 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work5 天前
ch21 签名、校验与发版
kotlin
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui