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多信号 combinecombineTransform
setCarProperty(propId, value, areaId, needCheck, needEmit)L162写入;needEmit=trueUI 先行

信号上行的转换(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 多了 isEnablederrorCode,方便 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-502combine(...) 把 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/settingsVehicleLibbase/settingsBaseLibmicarChargeSettingsmicarDrivingSettings
CarPropertyRepo1(定义源)071
mapPropFlow1(定义)051
collectIn01(FlowBinding.kt 助手)71

具体:

  • 定义层base/settingsVehicleLibCarPropertyRepo.kt
  • 公共助手base/settingsBaseLibFlowBinding.ktcollectIn
  • 消费层主要在 micarChargeSettings(7 文件,最重):EnergyDisplayModeUseCase / ParkPowerGenerationUseCase / EnergyTitleInfoUseCase / EnergyChargeInfoUseCase / EnergyChargeReportViewModel / EnergyInfoViewModel / EnergyPreheatController
  • 已扩散到 micarDrivingSettingsWetModeRecommendNotifyController.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
充电 ViewModelsettingsPage/micarChargeSettings/.../energy/viewmodel/EnergyInfoViewModel.kt
充电 ControllersettingsPage/micarChargeSettings/.../energy/controller/BaseEnergyInfoTitleController.kt
混合模式范例settingsPage/micarChargeSettings/.../energy/controller/EnergyCurrentController.kt