Appearance
modularization-spi-demo
1. 实验目标
用最小 Android Host 入口验证“接口下沉 + SPI/注册表寻址”的模块化解耦思路(对应 docs/05-bridge-hybrid/07):
- 接口下沉:调用方只依赖
IAnalyticsService/IFeatureRouter契约,不依赖具体实现类 - 运行时发现:通过 SPI 注册表模式在运行时找到 Analytics 实现与目标模块路由
- 模块解耦:证明“业务模块之间不直接引用实现类,也能完成埋点广播与路由寻址”
当前宿主为单模块演示骨架:为了在
Android Host中直接跑通,本实验用SpiRegistry模拟ServiceLoader/META-INF/services发现过程;真实多模块工程中,再把注册表替换为标准 Java SPI 或路由框架生成表。
2. 工程要点
SpiArchitectureDemoActivity:Compose 页面,提供两个动作按钮- 广播埋点事件:发现多个
IAnalyticsService实现并逐个调用 - 跨模块路由:按模块名查找
IFeatureRouter实现并返回目标路由
- 广播埋点事件:发现多个
IAnalyticsService/IFeatureRouter:下沉契约层,调用方永远只面向接口SpiRegistry:当前用内存注册表模拟运行时发现;注释里已标出正式多模块时应迁移到META-INF/services/...- MainActivity / AndroidManifest 已接通入口:宿主 App 中可直接点击“打开 SPI 与模块化架构实验室”验证
3. 运行方式
bash
cd labs/android/host
./gradlew installDebug设备/模拟器打开 Android Host → 打开 SPI 与模块化架构实验室。
验证步骤:
- 点击“广播埋点事件 (多扩展发现)”
- 观察日志区列出 2 个 Analytics 实现
- 两个实现都会收到同一事件
App_Launch
- 点击“跨界解耦路由 (寻址)”
- 观察日志区输出
OrderModule -> /order/detail_page
- 观察日志区输出
- 对照源码理解边界
- 调用方没有直接 new 具体实现类
- 扩展点靠契约与注册表收口,不靠模块硬引用
4. 预期现象
- 第一次按钮点击后,日志区显示“通过 SPI 发现 2 个 Analytics 实现”
- 随后依次打印:
FirebaseAnalytics (Remote Module)远端埋点模拟结果LocalDBAnalytics (Offline Module)本地 WAL/离线埋点模拟结果
- 第二个按钮点击后,日志区显示模块路由寻址成功:
OrderModule -> /order/detail_page
5. 常见误区
| 误区 | 实际情况 |
|---|---|
| 模块化就是把包名拆开 | 真正关键是“依赖方向”被反转:调用方依赖契约,不依赖实现 |
| 只要用了 ServiceLoader 就自动模块化 | SPI 只是发现机制;接口下沉、边界收口、依赖隔离才是核心 |
| 单模块骨架演示没有价值 | 这类 lab 的价值是先验证调用关系与解耦心智,再逐步迁移到真实多 module / dynamic feature 工程 |
| 路由框架一定优于 SPI | 页面跳转、服务发现、事件广播是不同问题;SPI / 路由 / EventBus 各有适用边界 |
6. 对应知识库文档
7. 关键实现速览
7.1 下沉契约:调用方只认接口
kotlin
interface IAnalyticsService {
val serviceName: String
fun trackEvent(event: String, properties: Map<String, Any>): String
}
interface IFeatureRouter {
val moduleName: String
fun getDestination(): String
}为什么看这段:它定义了“模块间只通过契约通信”的最小骨架,是后续替换成真实多模块 SPI 的前提。
7.2 注册表模拟运行时发现
kotlin
object SpiRegistry {
fun getAnalyticsServices(): List<IAnalyticsService> {
return listOf(FirebaseAnalyticsServiceImpl(), LocalDBAnalyticsServiceImpl())
}
fun getRouter(moduleName: String): IFeatureRouter? {
val routers = listOf(OrderFeatureRouterImpl())
return routers.find { it.moduleName.equals(moduleName, ignoreCase = true) }
}
}为什么看这段:当前宿主并非真实多模块工程,所以用注册表模拟 ServiceLoader.load(...);文档里可以先学解耦关系,再过渡到正式 SPI。
7.3 UI 验证层:一键触发多实现发现 / 跨模块寻址
kotlin
val services = SpiRegistry.getAnalyticsServices()
services.forEach { service ->
val result = service.trackEvent("App_Launch", mapOf("source" to "spi_lab"))
newLogs.add(" -> $result")
}为什么看这段:它直接证明调用层不需要 import 具体业务模块类型,也能把同一事件分发给多个实现。
完整源码:
8. 与后续样板的边界
- 本实验是 CLOSURE 型任务:补齐已有源码入口、README、文档回链,证明 05 模块化 / SPI 不是空地
- 它不替代后续“页面跳转协议 / 支付结果回流 / 分享权限桥 / 归因透传”这些真实业务桥接样板
- 它也不等于真正的 Dynamic Feature / 多 module Gradle 工程;那部分继续由
08-dynamic-delivery-hot-update与后续专项实验承接