04 - Flow 响应式架构(充电新模式)
本篇讲项目里较新的信号驱动 UI 模式——基于 Kotlin Coroutines StateFlow + UseCase + ViewModel。目前仅在
micarChargeSettings(充电)模块落地,但代表了项目的演进方向。与之相对的传统回调模式见 03 Controller 与 UI 刷新范式。两套机制并存,业务可按复杂度选用。
1. 为什么要引入 Flow 模式
传统回调模式(onHandlePropertyChange)适合一个信号控制一个开关的简单场景。但充电页这种场景需要:
- 一次订阅 10+ 个信号(SOC、充电状态、电缆类型、剩余时间、放电状态…)
- 多信号组合判定 UI 状态(如”正在充电” = SOC 在变 + 状态码 + 电缆连接)
- 跨页面共享信号处理逻辑
回调模式写这种组合逻辑会变成深嵌套 if-else,难维护。Flow 模式用 combine 把多路信号流拼成一条 UI 状态流,链式声明,清晰可测。
2. 整体链路
CarPropertyRepo (object 单例,信号 → StateFlow 的转换层)
│ 内部持有一个全局 SettingsCarPropertyManager,注册 CarPropertyManagerCallback
│ 把每个 propId 包装成 MutableStateFlow<CarPropertyValue<*>>
▼
mapPropFlow(propId) { ... } // 单信号 map
mapPropFlow(prop1 to area, prop2 to area, ...) { ... } // 多信号 combine
▼
UseCase (业务逻辑层,把原始信号流转换为 UI 状态流)
│ combine / map / transform
▼
ViewModel (stateIn 把 Flow 转成 StateFlow 暴露)
│ 持有 viewModelScope
▼
Controller / Fragment
mViewModel.xxxState.collectIn(viewLifecycleOwner) { 刷 UI }
3. 核心组件
3.1 信号源:CarPropertyRepo
路径:base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/CarPropertyRepo.kt
object单例(L22)- 内部持有全局
SettingsCarPropertyManager(L26-27),在init块注册CarPropertyManagerCallback(L35-62)——它本身也走主通道,只是把回调转成了 Flow mPropertyFlowMap: HashMap<String, MutableStateFlow<CarPropertyValue<*>>>(L28)——每个 propId 一个 StateFlow
关键方法:
| 方法 | 行号 | 作用 |
|---|---|---|
obtainPropertyFlow(propId, areaId) | L70-93 | 获取/创建一个 StateFlow;首次访问自动向 SettingsCarPropertyManager 注册该 propId |
mapPropFlow(propId, action) | L100 | 单信号 Flow → UI 状态 Flow |
mapPropFlow(vararg propAndArea: Pair<Int,Int>, action) | L106-116 | 多信号 combine(combineTransform) |
setCarProperty(propId, value, areaId, needCheck, needEmit) | L162 | 写入;needEmit=true 时UI 先行 |
信号上行的转换(L52-55):
override fun onPropertyChanged(propValue: CarPropertyValue<*>?) {
propValue ?: return
obtainPropertyFlow(propValue.propertyId).tryEmit(propValue)
}3.2 “UI 先行”写入机制
CarPropertyRepo.setCarProperty(L118-160)是 Flow 模式的写入入口,与传统模式的最大区别:
fun setCarProperty(propId, value, areaId, needCheck, needEmit = true) {
if (needEmit) {
obtainPropertyFlow(propId, areaId).tryEmit(包装的 value) // ① 先 emit,UI 立刻联动
}
// ② 再异步下发到车控
if (服务未连接) {
mSetPropertyQueue.add(...) // 入队,连接后补发
} else {
mCarPropertyManager.setCarProperty(...)
}
}传统模式是”下发 → 等回弹 → 刷 UI”,有延迟;Flow 模式是”先刷 UI → 再下发”,用户体验更跟手。失败时由 onSetPropertyTimeout / onSetPropertyError 回退(同 03 篇 的 restorePropertyToPrevious)。
3.3 订阅辅助:collectIn
路径:base/settingsBaseLib/src/main/java/com/android/car/settings/miauto/uitls/FlowBinding.kt
inline fun <reified STATE> Flow<STATE>.collectIn(
owner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline action: suspend (state: STATE) -> Unit,
): Job = owner.lifecycleScope.launch {
owner.repeatOnLifecycle(state = minActiveState) {
collectLatest { action(it) }
}
}这是 UI 层订阅 Flow 的标准姿势:
- 绑定
viewLifecycleOwner(不是 Fragment 本身),避免视图销毁后泄漏 STARTED态以上才收集,避免后台刷新- 用
collectLatest,新值到来时取消旧协程
3.4 Flow 模式的数据载体:CarPropertyData<T>
CarPropertyRepo.kt:194
data class CarPropertyData<T>(
val value: T,
val isEnabled: Boolean,
val errorCode: Int
)比裸 CarPropertyValue 多了 isEnabled 和 errorCode,方便 UI 直接用。
4. 完整范例(充电页)
4.1 UseCase:多信号合并
settingsPage/micarChargeSettings/src/main/java/com/android/micar/settings/energy/usecase/EnergyTitleInfoUseCase.kt
// L34-46 一次订阅 10 个信号,areaId=0
private val mChargeInfoState by lazy {
CarPropertyRepo.mapPropFlow(
BATTERY_SOC to DEFAULT_AREA_ID,
CHARGE_STATUS to DEFAULT_AREA_ID,
CABLE_CONNECTION_TYPE to DEFAULT_AREA_ID,
CHARGE_REMAIN_TIME_MINUTE to DEFAULT_AREA_ID,
DISCHARGE_STATUS to DEFAULT_AREA_ID,
// ... 共 10 个
) { props ->
val batterySocVal: Int = props[0].intValue()
val chargeStatusVal: Int = props[1].intValue()
// ... 把原始信号合并判定为 EnergyChargeInfo.ChargeInProgress(...) 等 UI 状态
}
}更复杂的组合(EnergyChargeInfoUseCase.kt:62-91):
mElectricBatteryMileageState =
CarPropertyRepo.mapPropFlow(BATTERY_SOC, MILEAGE_FOR_CLTC, ...) { it }
.combine(mDisplayModeState) { ... } // 再叠加"显示模式"业务状态EnergyChargeInfoUseCase.kt:406-502 用 combine(...) 把 9 个子 Flow 合并成最终展示数组——纯 Flow 流水线,无 UI 逻辑。
4.2 ViewModel:暴露 StateFlow
settingsPage/micarChargeSettings/src/main/java/com/android/micar/settings/energy/viewmodel/EnergyInfoViewModel.kt
class EnergyInfoViewModel : ViewModel() {
companion object {
fun getInstance(fragment: Fragment) =
ViewModelProvider(fragment).get(EnergyInfoViewModel::class.java) // 绑 Fragment 生命周期
}
// L64-65 注入 UseCase
private val mEnergyChargeInfoUseCase = EnergyChargeInfoUseCase(mIsChargePageState)
// L67-73 把 UseCase 的 Flow 用 stateIn 转成 StateFlow 暴露给 UI
val mChargeInfoStates by lazy {
mEnergyChargeInfoUseCase.mChargeInfoStates.stateIn(
viewModelScope, SHARING_STARTED, arrayOf()
)
}
// L139-143 init 里订阅需要主动响应的信号
init {
CarPropertyRepo.mapPropFlow(CABLE_CONNECTION_TYPE) { ... }
.launchIn(viewModelScope)
}
}注:
EnergyInfoViewModel同时暴露MutableLiveData(兼容老代码observe)和MutableStateFlow(新 UseCase 链路)——这是过渡期的常见做法。
4.3 Controller:订阅 StateFlow 刷 UI
settingsPage/micarChargeSettings/src/main/java/com/android/micar/settings/energy/controller/BaseEnergyInfoTitleController.kt
// L48-59 把 Flow 绑定到 viewLifecycleOwner
override fun onViewCreatedInternal() {
super.onViewCreatedInternal()
mViewModel.mTitleInfoState.collectIn(fragmentController.hostFragment.viewLifecycleOwner) {
preference.setEnergyInfo(getEnergyInfo(it)) // ← 信号变化最终落点
}
}或开关型(InCarDischargeSwitchController.kt:60-65):
mEnergyInfoViewModel.mInCarDischargePowerOptimize.collectIn(
fragmentController.hostFragment.viewLifecycleOwner
) { setSummary(it) }5. 混合模式(过渡期最常见)
老 CarPropertyMgrPreferenceController 接入新 ViewModel 的写法——EnergyCurrentController.kt:
- 同时用
CarPropertyMgrPreferenceController(信号回调 L128-156)和EnergyInfoViewModel(LiveData observe L110-112) - L96-98:
mViewModel by lazy { EnergyInfoViewModel.getInstance(getFragmentController().hostFragment) } - L110-112:
mViewModel.getEnergyInfoPageType().observe(fragmentController.hostFragment) { onEnergyInfoPageTypeChange(it) }
这种”老 Controller + 新 ViewModel + LiveData/StateFlow 并存”是当前最多见的形态。
6. 使用范围(实测,经对抗核验修正)
Flow 三件套的使用范围跨 4 个模块(2 base 库 + 2 settings page):
| 符号 | base/settingsVehicleLib | base/settingsBaseLib | micarChargeSettings | micarDrivingSettings |
|---|---|---|---|---|
CarPropertyRepo | 1(定义源) | 0 | 7 | 1 |
mapPropFlow | 1(定义) | 0 | 5 | 1 |
collectIn | 0 | 1(FlowBinding.kt 助手) | 7 | 1 |
具体:
- 定义层在
base/settingsVehicleLib(CarPropertyRepo.kt) - 公共助手在
base/settingsBaseLib(FlowBinding.kt的collectIn) - 消费层主要在
micarChargeSettings(7 文件,最重):EnergyDisplayModeUseCase/ParkPowerGenerationUseCase/EnergyTitleInfoUseCase/EnergyChargeInfoUseCase/EnergyChargeReportViewModel/EnergyInfoViewModel/EnergyPreheatController - 已扩散到
micarDrivingSettings:WetModeRecommendNotifyController.kt用了CarPropertyRepo+mapPropFlow
📌 订正:早期探索 agent 说”Flow 仅在充电模块”不准确。实际以充电为主,已外溢到驾驶模块。但灯光、锁、车身等绝大多数模块仍是传统回调模式。Flow 是”新模式”,正在缓慢推广。
7. 新页面该用哪套
| 场景 | 推荐 |
|---|---|
| 单信号控制一个开关/Tab | 传统回调(03 篇),代码最少 |
| 多信号组合判定 UI 状态 | Flow 模式(本篇) |
| 需要跨页面共享信号逻辑 | Flow + 共享 ViewModel |
| 老页面加新功能 | 混合模式,老 Controller 接 ViewModel |
详见 09 开发模板。
8. 关键路径速查
| 主题 | 路径 |
|---|---|
| Flow 信号仓库 | base/settingsVehicleLib/.../vehicle/CarPropertyRepo.kt |
| collectIn 扩展 | base/settingsBaseLib/.../miauto/uitls/FlowBinding.kt |
| 充电 UseCase 范例 | settingsPage/micarChargeSettings/.../energy/usecase/EnergyChargeInfoUseCase.kt |
| 充电 ViewModel | settingsPage/micarChargeSettings/.../energy/viewmodel/EnergyInfoViewModel.kt |
| 充电 Controller | settingsPage/micarChargeSettings/.../energy/controller/BaseEnergyInfoTitleController.kt |
| 混合模式范例 | settingsPage/micarChargeSettings/.../energy/controller/EnergyCurrentController.kt |