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.applicationcom.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_ 开头
}

命名空间冲突(namespaceapplicationId 不一致)也会引发 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 cataloggradle/libs.versions.toml)统一管理所有组件和依赖版本。


9. 组件内 Fragment 的管理

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

解法

  • 组件对外暴露的不是 Fragment,而是带路由 Path 的 Activity 或通过接口返回一个 Fragment 实例。
  • 使用 FragmentManagerfindFragmentById 在运行时动态获取。
  • 组件内 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 集中管理
相关推荐
2501_915909067 小时前
深入解析Mock.js:功能、应用及实战案例,提升前端开发效率
android·ios·小程序·https·uni-app·iphone·webview
流星白龙9 小时前
【MySQL高阶】21.撤销表空间,撤销日志
android·mysql·adb
我命由我1234510 小时前
Android 开发,FragmentPagerAdapter 的 isViewFromObject 方法问题
android·java-ee·kotlin·android studio·android jetpack·android-studio·android runtime
weiggle10 小时前
第五篇:Modifier 解析——链式调用的艺术
android
awu的Android笔记10 小时前
Android 弱网模拟:别只会用均匀分布——三种延迟模型和两种丢包模型的原理与实现
android·tcp/ip
sensor_WU11 小时前
【Delphi】 开发 android 升级模块硬核实现
android·delphi android·android 升级·apk升级 delphi
帅次11 小时前
Kotlin MVVM 实战入门:从分层到状态闭环
android·kotlin·android studio·android jetpack
YF021111 小时前
Android BLE 信号强度获取与 底层原理深度解析
android·蓝牙