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 包都是组件化
如果你想真正做组件化,关键三步:
- 拆出 api 模块,impl 只对内,api 对外暴露契约
- 组件能发布到 Maven ,其他项目
implementation 'com.xx:component:1.0'就能用 - 组件能独立运行 (
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 集中管理 |