Android 组件化 笔记

12 阅读7分钟

一、组件化的核心目标

在开始实现之前,我们需要明确组件化要达成什么:

  1. 业务模块间无直接依赖feature_home 不能直接依赖 feature_mine,它们只能依赖基础层(common)和服务接口层(service)。
  2. 模块可独立运行:每个业务模块在开发阶段可以作为一个独立的App启动和调试。
  3. 模块间通信标准化:页面跳转、服务调用、事件通知都有统一的、类型安全的方式。
  4. 生命周期可管理:组件可以在主App启动时按需初始化。

为了实现这些目标,我们需要引入一些核心技术。


二、运行时解耦:路由与服务化

这是组件化的基石。业务模块之间不能有直接的类引用,那么 feature_home 如何跳转到 feature_mineMineActivity?又如何调用 feature_mine 提供的获取用户信息的服务?

答案是路由框架 + 服务化(SPI)

1. 路由框架:解决页面跳转

路由框架的核心作用是通过一个中央的路由器,根据URL或路径找到并打开目标页面,从而避免直接引用目标Activity类。

原理简析

绝大多数路由框架(如ARouter、TheRouter、Butterfly)都采用**编译时注解处理器(APT)**生成路由表,然后在运行时通过类名加载目标类。

步骤拆解

  1. 定义注解:如 @Route(path = "/mine/main")
  2. 注解处理器:在编译时扫描所有带有 @Route 的类,生成路由表类(如 Router_Group_mine),里面记录了路径与Activity类的映射关系。
  3. 路由加载:在Application初始化时,通过类名(如 com.alibaba.android.arouter.routes.ARouter$$Root)加载这些生成的路由表类,存入内存中的Map。
  4. 执行跳转:当调用 ARouter.getInstance().build("/mine/main").navigation() 时,框架根据路径从Map中找到目标Activity类,然后通过 Intent 启动。

代码示例(以ARouter为例)

// 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),调用方通过路由框架获取接口的实现实例。

实现方式

  1. 定义服务接口(放在 :service_user 模块)
interface IUserService {
    fun getUserName(): String
    fun isLoggedIn(): Boolean
}
  1. 实现服务接口(放在 :feature_mine 模块)
@Route(path = "/service/user", name = "用户服务")
class UserServiceImpl : IUserService {
    override fun getUserName(): String {
        return "当前登录用户"
    }

    override fun isLoggedIn(): Boolean {
        return true
    }
}
  1. 调用服务(在 :feature_home 模块)
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 中定义一个开关:

# feature_mine/gradle.properties
isRunAlone=true  # true表示独立运行,false表示集成到主App

然后在模块的 build.gradle 中根据开关动态切换:

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时,这些应该被主模块的清单合并。解决方案是使用多个清单文件

目录结构如下:

feature_mine/
├── src/
│   ├── main/
│   │   ├── java/...
│   │   ├── res/...
│   │   └── AndroidManifest.xml      # 作为library时的清单(无入口,无application标签)
│   └── debug/                        # 独立运行时使用的源集
│       └── AndroidManifest.xml        # 包含入口Activity和application标签

清单文件内容示例

  • main/AndroidManifest.xml(library模式):
<manifest package="com.example.feature.mine">
    <application>
        <activity android:name=".MineActivity" />
    </application>
</manifest>
  • debug/AndroidManifest.xml(application模式):
<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下的清单:

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为例,它本身就有初始化方法:

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(this)  // 初始化路由
    }
}

对于自定义组件的初始化,可以定义一个接口:

interface IComponent {
    fun init(context: Context)
}

然后在每个组件中实现,并在主Application中通过反射或路由获取所有实现类并调用。不过更简单的做法是依赖注入框架,如Hilt,它可以很好地管理组件的作用域和生命周期。


四、组件化架构的整体视图

下面是一个典型的组件化工程结构,供你参考:

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 来强制检查:
    android {
        resourcePrefix "mine_"
    }
    

2. 依赖传递的控制

  • 基础模块对外暴露的接口类应该使用 api,而具体实现库(如Retrofit、Glide)应该使用 implementation,避免污染上层模块。
  • 可以使用Gradle的check任务或第三方插件检测循环依赖。

3. 组件通信的边界

  • 除了路由和服务,事件总线(如 LiveDataFlowEventBus)也可以用于模块间通信,但要慎用,因为它会引入隐式的依赖,不利于维护。
  • 多进程场景考虑 Andromeda

4. 独立运行时的调试

  • 为每个独立运行的业务模块配置单独的 applicationId 和应用图标,避免安装时覆盖主App。
  • 可以在 debug 源集中添加模拟数据或mock服务实现,方便独立测试。

5. 版本管理

  • 使用 Version Catalog 统一管理所有模块的依赖版本,避免版本冲突。

六、总结

组件化的实现是一场从工程结构到运行时机制的全面升级。它的核心可以概括为两点:

  1. 编译时:通过Gradle配置,让业务模块能在applicationlibrary间灵活切换,实现独立开发和调试。
  2. 运行时:通过路由框架(如ARouter)实现页面跳转的解耦,通过服务化(SPI)实现业务逻辑调用的解耦。

当你完成这些改造后,你的项目将获得:

  • 并行开发能力:多个团队可以独立开发和测试自己的业务模块。
  • 编译速度提升:修改单个模块只需编译该模块,无需全量编译。
  • 代码复用与隔离:模块间边界清晰,降低耦合,提高可维护性。

当然,组件化不是一蹴而就的,它需要团队有良好的规范和持续的重构意愿。希望这份详细的实现指南能帮你顺利落地组件化。如果有具体的技术选型或踩坑问题,随时再聊!