Android 模块化架构与依赖边界
Android 模块化架构与依赖边界
随着业务增长,单一模块会逐渐出现编译缓慢、代码相互引用、资源命名冲突和改动影响范围过大的问题。模块化可以隔离业务边界,但模块数量越多并不代表架构越好。真正有价值的模块化,应让依赖方向清晰、公共接口稳定,并降低团队并行开发时的冲突。
一、先识别模块边界
常见模块可以按职责划分:
- 应用壳模块:负责进程入口、全局装配和最终打包。
- 业务模块:围绕账户、交易、资讯等完整业务能力组织。
- 基础能力模块:网络、数据库、日志、主题等通用能力。
- 公共界面模块:项目统一控件、样式和资源。
- 纯领域模块:业务模型与规则,不依赖 Android 界面。
边界应来自业务变化频率和团队协作方式,而不是按每个页面建立一个模块。过细拆分会增加构建配置和跨模块跳转成本。
二、让依赖方向保持单向
业务模块可以依赖稳定的基础接口,基础模块不应反向知道具体业务。两个业务模块如果相互依赖,通常说明公共契约或流程协调职责没有被正确提取。
例如,订单模块只依赖账户读取能力:
interface AccountSession {
fun currentUserId(): String?
fun isLoggedIn(): Boolean
}
class SubmitOrderUseCase(
private val accountSession: AccountSession,
private val orderRepository: OrderRepository
) {
suspend operator fun invoke(order: PendingOrder): SubmitResult {
val userId = accountSession.currentUserId()
?: return SubmitResult.LoginRequired
return orderRepository.submit(userId, order)
}
}
账户模块或应用装配层提供实现,订单模块只看到接口,因此不需要引用账户页面和内部存储细节。
三、区分 API 与 implementation
依赖如果通过模块公共接口暴露给下游,应使用公开依赖;仅在模块内部使用的库和实现应保持内部依赖。过度暴露会扩大编译影响范围,也会让下游代码意外依赖传递细节。
判断方式很直接:下游模块编译自己的公共代码时,是否必须知道这个类型。如果不需要,就不应暴露。
四、控制模块公共表面
模块公共类越多,后续重构越困难。业务模块可以只公开少量入口、路由参数和结果类型,其余页面、适配器、数据库实体保持模块内部可见。
data class OrderDetailArgs(
val orderId: String,
val source: OrderSource
)
interface OrderNavigator {
fun openOrderDetail(args: OrderDetailArgs)
}
路由参数应使用稳定业务字段,不要直接传递整个可变实体。目标模块可以根据标识加载权威数据,也能减少跨模块序列化耦合。
五、避免建立万能公共模块
当任何不好归类的代码都被放进 common,这个模块最终会依赖所有基础能力,并被所有业务依赖,成为新的单体。公共模块只应包含语义稳定、确实被多个模块复用的内容。
复用一段代码并不总值得提取。如果两个业务当前实现相似,但未来变化原因不同,保留各自实现可能更合理。
六、管理资源与主题
多模块中的布局、颜色和图片容易重名。资源名称应遵循项目既有前缀规范,业务模块优先使用公共主题属性,而不是复制颜色值。
公共组件中的 TextView 宽高应优先使用 wrap_content,字号使用 dp,再通过约束控制可用空间,避免模块复用到不同屏幕时发生裁剪。固定尺寸只用于设计明确且经过适配验证的区域。
模块不应覆盖含义模糊的全局资源名,否则合并资源时可能产生难以察觉的样式变化。
七、使用依赖注入完成装配
业务模块声明所需接口,应用层决定使用哪个实现。无论项目采用手工工厂还是依赖注入框架,都应让装配集中可查。
class OrderFeatureFactory(
private val accountSession: AccountSession,
private val orderRepository: OrderRepository
) {
fun createSubmitOrderUseCase(): SubmitOrderUseCase {
return SubmitOrderUseCase(accountSession, orderRepository)
}
}
不要在业务代码内部到处读取全局单例,这会隐藏真实依赖并增加测试替换难度。
八、控制构建配置重复
每个模块复制一份插件、编译版本和静态检查配置,会导致升级时遗漏。可以使用项目已有的构建约定统一公共配置,让模块脚本只表达自身差异。
新增构建能力前必须确认与项目最低兼容版本、当前构建工具版本相符,不能为了局部便利破坏整体构建环境。
九、逐步实施模块化
大型项目不适合一次性移动所有代码。可以从依赖相对独立、边界清楚的业务开始:
- 记录当前模块依赖和跨业务调用。
- 定义少量稳定公共契约。
- 把实现迁移到新模块,保持行为不变。
- 删除旧入口并运行完整验证。
- 观察编译时间与协作冲突是否改善。
迁移期间避免同时进行大规模业务改造,否则出现问题时很难区分是架构移动还是逻辑变化造成的。
总结
模块化的目标是建立边界,而不是追求模块数量。围绕业务能力拆分,保持单向依赖,缩小公共接口,并把实现装配集中到应用层,才能降低编译影响和修改风险。任何拆分都应通过构建速度、依赖复杂度和团队协作效果来验证价值。
- 点赞
- 收藏
- 关注作者
评论(0)