Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

组件化路由与模块通信

组件化不是把包拆成很多 module, 而是让业务边界, 依赖方向和发布协作更清晰. 中高级面试常追问路由, 通信和依赖治理.

一, 组件化解决什么问题

问题组件化目标
业务耦合feature 边界清晰
编译慢模块增量构建
多团队协作独立开发 / 测试
依赖混乱依赖方向可控

二, 模块拆分方式

  • app 壳工程: 组装入口.
  • feature module: 业务功能.
  • core/common: 基础能力.
  • api/contract: 跨模块接口.

依赖方向应从上层依赖抽象, 避免 feature 之间直接互相依赖.

三, 路由框架原理 (以 ARouter 类框架理解)

核心流程: 编译期扫描注解 → APT/KSP 生成路由表 → 运行时按 path 查找目标 → 拦截器处理登录/权限/降级 → 跳转或返回服务实例.

APT/KSP 生成表示意 (伪代码, 仅解释生成物, 不声称可编译):

@Route(path = "/user/profile") ProfileActivity
          -> processor scans feature-user
          -> generated RouteTable { "/user/profile" -> ProfileActivity::class }
          -> app 打包 feature-user-impl 的生成表,并在启动时显式注册;若框架用类路径发现,则需保证生成表未被裁剪

演进提示: ARouter 官方维护已趋缓 (最后一次实质更新约 2021 年前后), 社区有迁移 AndroidX / 适配新 AGP 的维护 fork. 新一代方案 (KRouter 等) 多转向 KSP 编译期生成路由表, 强调类型安全与减少运行期反射 / 扫描; 各方案现状以社区 / 官方最新文档为准, 面试讲清选型理由即可.

工程权衡: 路由表规模与冷启动注册耗时成正比, 路由表越大启动扫描 / 注册成本越高, 大项目需懒加载或异步注册, 避免阻塞冷启动.

interface UserService {
    fun currentUserId(): String?
}

跨模块不要直接调用实现类, 而是依赖 UserService 这样的 contract.

四, 模块通信方式

方式适用风险
路由跳转页面级跳转path 字符串错误
接口下沉同步能力调用contract 膨胀
事件总线广播式通知难追踪, 生命周期问题
Result API页面结果回传复杂链路难维护

五, 路由拦截, 降级与灰度

登录校验, 权限校验, A/B 实验, 页面不存在降级都适合放在固定顺序的路由 pipeline 中: 规范化参数 -> 登录 -> 权限/风控 -> 实验/动态模块 -> 最终 navigator. 路由失败必须有兜底, 不能直接 crash.

上下文片段: 这是与具体路由库无关的顺序 pipeline 伪代码 (不是带 proceed() 的责任链).每个步骤可变换 request 或短路返回; 预期: 未登录访问支付页时 LoginInterceptor 返回登录路由, 后续步骤和最终 navigator 不执行; 通过所有检查后才由 navigator 负责实际跳转.

interface RouteInterceptor { fun intercept(request: RouteRequest): RouteResult }
fun dispatch(request: RouteRequest, pipeline: List<RouteInterceptor>, navigator: Navigator): RouteResult {
    var current = request
    for (interceptor in pipeline) {
        val result = interceptor.intercept(current)
        if (result !is RouteResult.Continue) return result
        current = result.request
    }
    return navigator.navigate(current)
}

失败排查: 症状是页面未打开; 证据记录 path, 参数 schema, 拦截器名称和结果; 先区分未注册, 参数非法, 登录 / 权限拒绝和动态模块未安装; 修复为注册生成表, 校验 contract 或提供降级; 最后覆盖每条拦截分支与目标跳转.

六, 和 Gradle 工程化的关系

组件化设计关注业务边界; Gradle 工程化关注构建配置, 依赖版本, 产物和性能. 两者相关但不是一回事.

七, 现代组件化治理: 契约, 构建逻辑与类型安全路由

面试官往往不满足于 “用了 ARouter”, 会继续追问模块怎么拆, 依赖怎么防腐, 路由怎么治理.

依赖方向

下图中的箭头表示 Gradle 依赖方向: app 负责组装实现模块, feature-*-impl 依赖自身 feature-*-api, 其他 feature 只能依赖 API 契约, 不能依赖实现模块.

app
 ├─ feature-home-impl  ──> feature-home-api
 ├─ feature-pay-impl   ──> feature-pay-api
 └─ core-ui / core-network / core-common

从单体迁移时可分三步: 先在单模块内抽出 feature API 和实现包; 再移动实现到 feature module, 让 app 组装; 最后用 CI 检查 feature 不反向依赖 app 或其他 feature 的 impl. 正确依赖图如下 (箭头为 Gradle 依赖):

app -> feature-user-impl -> feature-user-api
app -> feature-pay-impl  -> feature-pay-api
feature-user-impl -> core-*
feature-pay-impl  -> core-*
other feature -> feature-*-api (never feature-*-impl)

单体到组件化的迁移图如下 (箭头为 Gradle 依赖):

before: app(ui + user + pay + data)
after:  app -> feature-user-impl -> feature-user-api
        app -> feature-pay-impl  -> feature-pay-api
        other feature -> feature-*-api
        feature-* -> core-common / core-ui / core-network
  • 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 / 插件化边界

动态交付适合低频, 体积大的功能, 但会带来下载失败, 版本兼容, 冷启动和路由降级问题. 面试中要强调: 动态化不是为了炫技, 而是为了控制包体积, 灰度风险或业务隔离.

路由治理清单

  1. path 命名规范和归属人.
  2. 参数 schema 与必填校验.
  3. 未命中, 版本不兼容, 未安装动态模块时的降级页.
  4. 登录/权限/风控拦截器顺序.
  5. 路由耗时, 失败率, 来源页面埋点.

编译期路由与 API 稳定性

评估路由方案不能只比较运行时反射能力. 可使用 KSP/注解生成编译期路由表, 但要评估增量构建, 错误提示和跨模块 API 演进. Dynamic Feature 取决于分发渠道和安装状态. 组件 API 应有 owner, 版本/废弃周期和依赖可视化, CI 阻止反向依赖与循环依赖; “彻底解耦” 不是目标, 可理解且可演进的契约才是目标.

高频面试题

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 要集中治理, 否则重构时容易断链.