第4章:模块间的状态所有权:谁创建谁拥有,谁消费谁订阅
跨模块状态交互的第一个原则就是确定 所有权边界。所有权由生命周期和创建时机决定:哪个模块负责创建 ViewModel 或状态容器,哪个模块就拥有该状态的生命周期。其他模块只能通过订阅的方式消费这些状态,而不能直接干预其创建或销毁流程。
核心定义:“谁创建谁拥有,谁消费谁订阅”是跨模块状态隔离的底线契约。越过这条底线,就会引发生命周期错配、内存泄漏和状态污染。
边界定义
所有权边界由 模块依赖关系 和 生命周期作用域 共同界定。模块 A 在 DI 容器中创建 ViewModel,模块 B 和 C 通过接口订阅该 ViewModel 的状态,而不是直接持有 ViewModel 的引用。
模块 A 的销毁信号发出后,所有订阅该状态的模块必须停止数据采集,否则会产生悬挂引用。
| 维度 | 拥有状态(提供者) | 消费状态(订阅者) |
|---|---|---|
| 生命周期责任 | 创建 & 销毁 | 附加 & 分离 |
| 状态突变 | 允许(内部) | 禁止(只读视图) |
| 线程安全 | 必须保证 | 不关心 |
| 测试策略 | 集成测试 | Mock / Fake 状态 |
分层策略
在跨模块分层架构中,状态通常驻留在 Core / Common 层 或通过 DI 容器 进行作用域绑定。Feature 模块只能通过接口依赖关系来消费状态,而不能在本地重新创建或持有缓存副本,这就是 “提升状态(State Hoisting)” 策略在模块间的应用。
// 公共 API 层(合约)
interface UserStateProvider {
val state: StateFlow<UserState>
}
// 实现层(由 Core 模块提供实现)
@Singleton
class UserStateProviderImpl(
private val repository: UserRepository
) : UserStateProvider {
override val state: StateFlow<UserState> = _state
private val _state = MutableStateFlow(UserState())
// ……
}
组件职责
- 状态提供者: 负责实例化
StateFlow,暴露安全的读取接口,处理业务逻辑,并决定状态更新的时机与粒度。 - 状态消费者: 在自身的生命周期回调中(如
onCreate或onStart)订阅collect,在生命周期结束时取消订阅。消费者通过动作 / 事件反馈给提供者,而不是直接赋值。
数据流 / 控制流 / 信任链路
数据流: 状态从提供者单向向下流向消费者。消费者通过 collect 接收快照。
控制流: 动作从消费者向上流向提供者。提供者经过业务逻辑处理后,产出新状态并推送给所有订阅者。
信任链路: 提供者信任消费者不会滥用状态令牌(例如通过反射修改),消费者信任提供者不会在生命周期中途意外死亡。双方通过不变的接口契约和严格的 DI 作用域来维持这种信任。
选型权衡
| 选型 | 优势 | 劣势 |
|---|---|---|
| StateFlow | 同步、挂起安全、粘性数据、跨模块友好 | 需要协程环境,不适合纯 Java 模块 |
| SharedFlow | 一次性事件(导航、Snackbar) | 无初始值,消费者需自行处理粘性防抖 |
| LiveData | 生命周期感知、Android 原生支持 | 不支持协程挂起,线程模型较弱 |
| 冷 Flow | 按需创建数据流 | 每个订阅者独立产生数据,不适合共享状态 |
风险点
生命周期错配: 如果消费者模块的 Fragment 在提供者模块的 ViewModel 之前被销毁,且未在
onDestroy 中取消订阅,则会造成内存泄漏或空指针异常。
状态泄露: 消费者意外保留了外部状态容器的引用,阻碍了 GC 回收。
惊群效应: 提供者频繁发出大量状态更新,导致所有订阅者不必要地执行多次重绘或计算。推荐使用去抖(debounce)或去重(distinctUntilChanged)策略。
落地步骤
- 审查模块依赖图: 标识出哪些模块需要共享状态,确定生命周期先后顺序。
- 定义合约接口: 在公共层创建接口,如
interface UserStateProvider,并声明状态流。 - 实现提供者: 在 Core / 实现模块中完成状态容器和业务逻辑的注入。
- 作用域绑定: 在 DI 容器中使用
@Singleton或自定义作用域进行绑定,确保提供者生命周期受控。 - 消费者注入: 消费者模块注入接口,并在其自身作用域内安全收集状态流。
// 步骤4 – Dagger / Hilt 绑定示例
@Module
@InstallIn(SingletonComponent::class)
abstract class StateModule {
@Binds
abstract fun bindUserStateProvider(
impl: UserStateProviderImpl
): UserStateProvider
}
// 步骤5 – 消费者订阅
@AndroidEntryPoint
class ProfileFragment : Fragment() {
@Inject lateinit var userProvider: UserStateProvider
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
userProvider.state
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { state -> /* 更新 UI */ }
}
}
}
验收标准
- 生命周期隔离: 提供者作用域结束后,所有订阅者的回调必须自动停止。
- 类型安全: 消费者无法直接写入提供者的状态容器(只读接口)。
- 稳定性: 跨配置变更(如屏幕旋转)后,订阅依然正常,不抛出空指针异常。
- 可测试性: 提供者的状态变更逻辑通过 Fake / Mock 消费者得到验证,测试覆盖率超过 80%。