组件化路由与模块通信
组件化不是把包拆成很多 module,而是让业务边界、依赖方向和发布协作更清晰。中高级面试常追问路由、通信和依赖治理。
一、组件化解决什么问题
| 问题 | 组件化目标 |
|---|---|
| 业务耦合 | feature 边界清晰 |
| 编译慢 | 模块增量构建 |
| 多团队协作 | 独立开发/测试 |
| 依赖混乱 | 依赖方向可控 |
二、模块拆分方式
- app 壳工程:组装入口。
- feature module:业务功能。
- core/common:基础能力。
- api/contract:跨模块接口。
依赖方向应从上层依赖抽象,避免 feature 之间直接互相依赖。
三、路由框架原理(以 ARouter 类框架理解)
核心流程:编译期扫描注解 → APT/KSP 生成路由表 → 运行时按 path 查找目标 → 拦截器处理登录/权限/降级 → 跳转或返回服务实例。
interface UserService {
fun currentUserId(): String?
}
跨模块不要直接调用实现类,而是依赖 UserService 这样的 contract。
四、模块通信方式
| 方式 | 适用 | 风险 |
|---|---|---|
| 路由跳转 | 页面级跳转 | path 字符串错误 |
| 接口下沉 | 同步能力调用 | contract 膨胀 |
| 事件总线 | 广播式通知 | 难追踪、生命周期问题 |
| Result API | 页面结果回传 | 复杂链路难维护 |
五、路由拦截、降级与灰度
登录校验、权限校验、A/B 实验、页面不存在降级都适合放在拦截器链里。路由失败必须有兜底,不能直接 crash。
六、和 Gradle 工程化的关系
组件化设计关注业务边界;Gradle 工程化关注构建配置、依赖版本、产物和性能。两者相关但不是一回事。
七、现代组件化治理:契约、构建逻辑与类型安全路由
面试官往往不满足于“用了 ARouter”,会继续追问模块怎么拆、依赖怎么防腐、路由怎么治理。
依赖方向
app
├─ feature-home-impl ──> feature-home-api
├─ feature-pay-impl ──> feature-pay-api
└─ core-ui / core-network / core-common
api/contract模块只放接口、路由契约、DTO,不放具体页面和重业务逻辑。impl模块实现页面和业务,依赖自己的 api 与公共 core。- feature 之间不直接依赖实现,避免循环依赖和“改 A 编译半个项目”。
Convention Plugin 与 Version Catalog
组件化后如果每个 module 都复制 Android/Kotlin/Compose 配置,会迅速失控。更好的做法是把公共配置沉淀到 Gradle convention plugin,并用 Version Catalog 管理依赖版本。
| 治理对象 | 推荐方式 | 面试表达 |
|---|---|---|
| compileSdk/minSdk/Kotlin 版本 | convention plugin | 全项目一致,减少漂移 |
| 依赖版本 | version catalog | 可审计、可统一升级 |
| lint/ktlint/测试配置 | convention plugin | 新模块默认接入质量门禁 |
| feature 开关 | 配置中心/构建变体 | 支持灰度和降级 |
类型安全路由契约
字符串 path 容易写错、参数缺失难发现。可以在 contract 层定义类型安全入口:
data class PayRoute(val orderId: String, val source: String)
interface PayNavigator {
fun openPay(route: PayRoute)
}
底层可以仍然用路由框架,但上层业务依赖的是类型化 contract,而不是到处拼 "/pay/detail?orderId=..."。
Dynamic Feature / 插件化边界
动态交付适合低频、体积大的功能,但会带来下载失败、版本兼容、冷启动和路由降级问题。面试中要强调:动态化不是为了炫技,而是为了控制包体积、灰度风险或业务隔离。
路由治理清单
- path 命名规范和归属人。
- 参数 schema 与必填校验。
- 未命中、版本不兼容、未安装动态模块时的降级页。
- 登录/权限/风控拦截器顺序。
- 路由耗时、失败率、来源页面埋点。
高频面试题
Q1:组件化和模块化区别? 答:模块化偏代码物理拆分,组件化更强调业务组件独立开发、独立测试、按契约通信和可组装。
Q2:ARouter 这类框架大概怎么实现? 答:编译期注解处理生成路由表,运行时初始化加载,按 path 找目标 class/provider,再通过拦截器处理统一逻辑。
Q3:跨模块通信为什么不直接依赖实现? 答:直接依赖会导致 feature 耦合、编译依赖变重、循环依赖风险;接口下沉能保持依赖方向稳定。
Q4:组件化项目怎么避免 Gradle 配置复制和依赖版本失控? 答:把公共 Android/Kotlin/Compose/lint/test 配置沉淀为 convention plugin,新模块默认套用;依赖版本放 Version Catalog 或统一依赖管理。这样既减少重复,也能在升级 compileSdk、Kotlin、AGP 时集中治理。
易错点 / 追问
- 不要为了组件化制造过多小 module。
- 不要把所有通信都塞进 EventBus。
- 路由 path 要集中治理,否则重构时容易断链。