前言
Android Automotive 项目中,CarPropertyManager 几乎是访问一切车辆状态的入口。但在真实项目里,如果直接在 ViewModel 或业务层大量使用它,代码会变得难以维护、难以测试,也完全不符合 现代Android 架构 的分层思想。
本文将从零开始,梳理一套基于 Clean Architecture 的 CarPropertyManager 封装方案,涵盖:
- 为什么不能直接用
- 如何用抽象基类统一封装
callbackFlow的正确姿势Flow与suspend如何取舍- Repository 的真正职责边界
1. 什么是 CarPropertyManager?
1.1 基本概念
CarPropertyManager 是 Android Automotive OS 提供的车辆属性访问入口,底层通过与 VHAL(Vehicle Hardware Abstraction Layer) 交互,读取或监听车辆的实时状态。
常见可访问的属性包括:
| 属性 | 说明 |
|---|---|
PERF_VEHICLE_SPEED |
车速 |
GEAR_SELECTION |
档位 |
EV_BATTERY_LEVEL |
电池电量(SOC) |
ENV_OUTSIDE_TEMPERATURE |
室外温度 |
DOOR_OPEN |
车门状态 |
INFO_VIN |
车辆 VIN 码 |
INFO_MODEL |
车辆型号 |
1.2 基础用法
读取单次属性值:

注册监听,持续接收变化:

取消监听:

看起来不复杂,问题在哪里?
2. 为什么不要在业务层直接使用 CarPropertyManager?
2.1 反例
很多项目一开始会写出这样的代码:

2.2 这样写有哪些问题?
① UI 层与 Framework 强耦合
CarPropertyManager 是 Automotive Framework API,ViewModel 不应该感知它的存在。一旦底层 API 变动,ViewModel 就得跟着改。
② 单元测试困难
CarPropertyManager 无法在 JUnit 环境中直接实例化,测试 ViewModel 需要大量 Mock,成本极高。
③ 生命周期管理散乱
registerCallback 和 unregisterCallback 分散在各处,极易出现忘记注销、重复注册或内存泄漏的问题。
④ 属性映射逻辑到处都是
每个属性的类型转换(value as Float、value as Int)散落在不同地方,没有统一的错误处理。
⑤ 大量重复代码
每个属性都要重写一套几乎相同的 registerCallback → callback → unregisterCallback 流程,毫无复用。
💡 我们需要一个统一的数据访问层,把
CarPropertyManager的使用细节彻底封装起来。
3. BaseCarPropertySource:统一封装的抽象基类
3.1 为什么用抽象基类?
观察所有车辆属性的访问流程,会发现结构完全一致:

唯一变化的只有两点:
propertyId:访问哪个属性mapValue:如何把原始值转成业务类型
这正是模板方法模式的最佳使用场景------稳定的流程放在基类,变化的部分留给子类实现。
3.2 基类设计

3.3 子类只需关心两件事

整个注册、监听、注销的细节,子类完全无需关心。
4. callbackFlow:将 Callback 正确转换成 Flow
4.1 为什么选 callbackFlow?
很多人会用 MutableSharedFlow + 手动管理生命周期的方式:

这种做法的问题在于:
- 注册和取消的时机不受 Flow 订阅者控制
- 无订阅者时仍在消耗资源
GlobalScope容易造成泄漏
callbackFlow 的优势:
| 特性 | callbackFlow |
|---|---|
| 订阅时才注册 | ✅ |
| 无订阅者自动取消注册 | ✅ |
| 生命周期与 collector 绑定 | ✅ |
| 背压处理 | ✅(trySend / buffer) |
4.2 高频属性的背压问题
⚠️ 注意:对于车速这类高频更新的属性,在低端硬件上可能出现 Flow 积压。
可以根据场景选择策略:

5. 静态属性为什么更适合 suspend,而不是 Flow
5.1 API 的命名应该符合实际行为
这类属性的正确表达方式是 suspend 函数:
kotlin
// ✅ 推荐
suspend fun getVin(): String
suspend fun getInfoModel(): String
suspend fun getManufacturer(): String
判断原则:
- 数据持续变化 ,需要持续监听 →
Flow- 数据一次性读取 ,几乎不变化 →
suspend fun
8. 完整架构总览
经过以上各层的设计,整体架构如下:
arduino
CarPropertyManager
│
▼
BaseCarPropertySource<T>
│
┌────────────┴────────────┐
│ │
▼ ▼
OutsideTemperatureSource InfoModelSource
(observe: Flow<Float>) (get: suspend String)
│ │
└────────────┬────────────┘
▼
CarRepository
(combine / map)
│
▼
DashboardInfo
(领域模型)
│
▼
ViewModel
(StateFlow)
│
▼
Compose UI
每一层的职责边界清晰:
| 层级 | 职责 |
|---|---|
CarPropertyManager |
Framework API,提供车辆属性访问能力 |
BaseCarPropertySource |
封装注册/回调/注销细节,转换为 Flow 或 suspend |
Repository |
聚合多个 Source,提供领域模型 |
ViewModel |
持有 UI 状态,处理 UI 事件 |
Compose UI |
展示数据,处理格式化 |
总结
CarPropertyManager是 Framework 层的访问入口,职责止于"能拿到数据";BaseCarPropertySource封装访问细节,职责止于"把数据变成 Flow 或值";Repository聚合业务所需的数据,职责止于"给 ViewModel 提供可直接使用的领域模型"。
好的架构设计,本质上是在做一件事:让每一层只承担它应该承担的职责,不多也不少。

下图是我设置外部温度的截图,上面的动图能看到变化.
