Android 组件化 笔记

一、组件化的核心目标

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

  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为例)

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

实现方式

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

    override fun isLoggedIn(): Boolean {
        return true
    }
}
  1. 调用服务 (在 :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 来强制检查:

    groovy 复制代码
    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)实现业务逻辑调用的解耦。

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

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

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

相关推荐
编程小风筝1 小时前
Android移动端如何实现多线程编程?
android
城东米粉儿2 小时前
Android 模块化 笔记
android
城东米粉儿2 小时前
Android HandlerThread 笔记
android
城东米粉儿2 小时前
Android Condition 笔记
android
肖。35487870942 小时前
html中onclick误区,后续变量会更改怎么办?
android·java·javascript·css·html
城东米粉儿3 小时前
Android 动态加载 Activity
android
城东米粉儿3 小时前
Android lancet 笔记
android
zh_xuan3 小时前
React Native 原生和RN互相调用以及事件监听
android·javascript·react native
哈哈浩丶5 小时前
LK(little kernel)-3:LK的启动流程-作为Android的bootloarder
android·linux·服务器