一、组件化的核心目标
在开始实现之前,我们需要明确组件化要达成什么:
- 业务模块间无直接依赖 :
feature_home不能直接依赖feature_mine,它们只能依赖基础层(common)和服务接口层(service)。 - 模块可独立运行:每个业务模块在开发阶段可以作为一个独立的App启动和调试。
- 模块间通信标准化:页面跳转、服务调用、事件通知都有统一的、类型安全的方式。
- 生命周期可管理:组件可以在主App启动时按需初始化。
为了实现这些目标,我们需要引入一些核心技术。
二、运行时解耦:路由与服务化
这是组件化的基石。业务模块之间不能有直接的类引用,那么 feature_home 如何跳转到 feature_mine 的 MineActivity?又如何调用 feature_mine 提供的获取用户信息的服务?
答案是路由框架 + 服务化(SPI)。
1. 路由框架:解决页面跳转
路由框架的核心作用是通过一个中央的路由器,根据URL或路径找到并打开目标页面,从而避免直接引用目标Activity类。
原理简析
绝大多数路由框架(如ARouter、TheRouter、Butterfly)都采用**编译时注解处理器(APT)**生成路由表,然后在运行时通过类名加载目标类。
步骤拆解:
- 定义注解 :如
@Route(path = "/mine/main")。 - 注解处理器 :在编译时扫描所有带有
@Route的类,生成路由表类(如Router_Group_mine),里面记录了路径与Activity类的映射关系。 - 路由加载 :在Application初始化时,通过类名(如
com.alibaba.android.arouter.routes.ARouter$$Root)加载这些生成的路由表类,存入内存中的Map。 - 执行跳转 :当调用
ARouter.getInstance().build("/mine/main").navigation()时,框架根据路径从Map中找到目标Activity类,然后通过Intent启动。
代码示例(以ARouter为例)
kotlin
// 1. 在目标模块(feature_mine)的Activity上添加注解
@Route(path = "/mine/main")
class MineActivity : AppCompatActivity() {
// ...
}
// 2. 在发起模块(feature_home)中跳转
ARouter.getInstance().build("/mine/main")
.withString("key", "value") // 携带参数
.navigation()
// 3. 接收参数(在MineActivity中)
@Autowired(name = "key")
lateinit var value: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ARouter.getInstance().inject(this) // 参数自动注入
Log.d("MineActivity", "received: $value")
}
高级能力
- Fragment跳转 :
ARouter.getInstance().build("/mine/fragment").navigation()返回Fragment实例。 - 拦截器:可实现登录拦截、埋点拦截等全局导航控制。
- 降级策略:当目标路径不存在时,可以跳转到统一的错误页。
2. 服务化(SPI):解决服务调用
页面跳转解决了UI层面的解耦,但业务逻辑的调用(如获取用户信息)同样需要解耦。这就要靠服务化。
服务化的核心思想是:面向接口编程 。接口定义在公共服务层(service模块),实现在具体的业务模块(feature_mine),调用方通过路由框架获取接口的实现实例。
实现方式
- 定义服务接口 (放在
:service_user模块)
kotlin
interface IUserService {
fun getUserName(): String
fun isLoggedIn(): Boolean
}
- 实现服务接口 (放在
:feature_mine模块)
kotlin
@Route(path = "/service/user", name = "用户服务")
class UserServiceImpl : IUserService {
override fun getUserName(): String {
return "当前登录用户"
}
override fun isLoggedIn(): Boolean {
return true
}
}
- 调用服务 (在
:feature_home模块)
kotlin
val userService = ARouter.getInstance().navigation(IUserService::class.java)
if (userService != null) {
val name = userService.getUserName()
textView.text = "欢迎,$name"
}
原理
路由框架在生成路由表时,同样会为实现了接口的服务类生成记录。当调用 navigation(Class) 时,框架根据接口类型查找对应的实现类,然后通过反射实例化并返回。这种方式类似于Java的 ServiceLoader,但更轻量且与路由体系整合。
多进程的考量
如果你的应用使用了多进程,普通的单进程路由就无法满足需求了。这时候需要更强大的框架,比如爱奇艺开源的 Andromeda,它同时支持本地服务和跨进程服务路由,并且能处理跨进程的回调。不过对于大多数App,单进程路由已经足够。
三、编译时独立:让业务模块可单独运行
在开发阶段,我们希望每个业务模块可以独立运行,以便快速调试和开发。这就需要通过Gradle配置,让模块在集成模式 (作为library)和组件模式(作为application)之间动态切换。
1. 动态切换插件和ApplicationId
在模块的 gradle.properties 中定义一个开关:
properties
# feature_mine/gradle.properties
isRunAlone=true # true表示独立运行,false表示集成到主App
然后在模块的 build.gradle 中根据开关动态切换:
groovy
if (isRunAlone.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
defaultConfig {
if (isRunAlone.toBoolean()) {
applicationId "com.example.feature.mine" // 独立运行时需要独立的applicationId
}
minSdk 21
targetSdk 34
}
}
2. 配置独立的AndroidManifest
作为application独立运行时,需要有入口Activity和Application;而作为library时,这些应该被主模块的清单合并。解决方案是使用多个清单文件。
目录结构如下:
bash
feature_mine/
├── src/
│ ├── main/
│ │ ├── java/...
│ │ ├── res/...
│ │ └── AndroidManifest.xml # 作为library时的清单(无入口,无application标签)
│ └── debug/ # 独立运行时使用的源集
│ └── AndroidManifest.xml # 包含入口Activity和application标签
清单文件内容示例:
main/AndroidManifest.xml(library模式):
xml
<manifest package="com.example.feature.mine">
<application>
<activity android:name=".MineActivity" />
</application>
</manifest>
debug/AndroidManifest.xml(application模式):
xml
<manifest package="com.example.feature.mine">
<application
android:name=".debug.DebugApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.MyApp">
<activity android:name=".MineActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
然后在 build.gradle 中配置sourceSets,让独立运行时使用debug下的清单:
groovy
android {
sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
// 只有独立运行时才使用debug源集的清单
if (isRunAlone.toBoolean()) {
debug {
manifest.srcFile 'src/debug/AndroidManifest.xml'
}
}
}
}
3. 处理Application的初始化
组件化后,主App的Application需要负责初始化各个组件。通常有两种方式:
- 手动调用 :在
onCreate()中逐个调用组件的初始化方法。 - 自动注册:利用APT生成组件列表,然后在Application中统一加载。
以ARouter为例,它本身就有初始化方法:
kotlin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
ARouter.openLog()
ARouter.openDebug()
}
ARouter.init(this) // 初始化路由
}
}
对于自定义组件的初始化,可以定义一个接口:
kotlin
interface IComponent {
fun init(context: Context)
}
然后在每个组件中实现,并在主Application中通过反射或路由获取所有实现类并调用。不过更简单的做法是依赖注入框架,如Hilt,它可以很好地管理组件的作用域和生命周期。
四、组件化架构的整体视图
下面是一个典型的组件化工程结构,供你参考:
csharp
MyApp/
├── app/ # 主模块,负责组装和初始化,不包含业务代码
├── buildSrc/ # 统一版本管理
├── common/ # 基础层
│ ├── common-base # 基础工具类
│ └── common-ui # 自定义UI组件
├── service/ # 公共服务层(接口定义)
│ ├── service_user # 用户服务接口
│ └── service_router # 路由路径常量(可选)
├── feature/ # 业务组件层
│ ├── feature_home
│ │ ├── src/
│ │ │ ├── main/
│ │ │ └── debug/ # 独立运行配置
│ │ └── build.gradle
│ ├── feature_mine
│ └── feature_order
└── libs/ # 第三方库(可选)
依赖关系:
app→ 所有feature模块(集成模式下)feature_*→service_*、common-*feature_*之间没有直接依赖,通过路由和服务通信
五、进阶话题与避坑指南
1. 资源冲突与命名
-
每个模块的资源文件建议加上模块前缀,例如
mine_activity_main.xml。 -
可以在
build.gradle中设置resourcePrefix来强制检查:groovyandroid { resourcePrefix "mine_" }
2. 依赖传递的控制
- 基础模块对外暴露的接口类应该使用
api,而具体实现库(如Retrofit、Glide)应该使用implementation,避免污染上层模块。 - 可以使用Gradle的
check任务或第三方插件检测循环依赖。
3. 组件通信的边界
- 除了路由和服务,事件总线 (如
LiveData、Flow、EventBus)也可以用于模块间通信,但要慎用,因为它会引入隐式的依赖,不利于维护。 - 多进程场景考虑 Andromeda。
4. 独立运行时的调试
- 为每个独立运行的业务模块配置单独的
applicationId和应用图标,避免安装时覆盖主App。 - 可以在
debug源集中添加模拟数据或mock服务实现,方便独立测试。
5. 版本管理
- 使用 Version Catalog 统一管理所有模块的依赖版本,避免版本冲突。
六、总结
组件化的实现是一场从工程结构到运行时机制的全面升级。它的核心可以概括为两点:
- 编译时 :通过Gradle配置,让业务模块能在
application和library间灵活切换,实现独立开发和调试。 - 运行时 :通过路由框架 (如ARouter)实现页面跳转的解耦,通过服务化(SPI)实现业务逻辑调用的解耦。
当你完成这些改造后,你的项目将获得:
- 并行开发能力:多个团队可以独立开发和测试自己的业务模块。
- 编译速度提升:修改单个模块只需编译该模块,无需全量编译。
- 代码复用与隔离:模块间边界清晰,降低耦合,提高可维护性。
当然,组件化不是一蹴而就的,它需要团队有良好的规范和持续的重构意愿。希望这份详细的实现指南能帮你顺利落地组件化。如果有具体的技术选型或踩坑问题,随时再聊!