03 - Controller 与 UI 刷新范式

本篇讲项目主流的信号驱动 UI 模式:基于 AndroidX Preference + PreferenceController 的回调模式。这是绝大多数设置页(灯光、锁车、车身、驾驶…)的写法。

新的 Flow 模式见 04 篇。两套并存。


1. 核心思想

项目不用 DataBinding / ViewBinding,而是用 AndroidX Preference 体系(实际是定制版 micarx.preference.PreferenceFragment):

  1. 页面用 XML 声明一组 Preference,每个 Preference 用 settings:controller="全限定类名" 绑定一个 Controller
  2. Fragment 在 onAttach 时反射实例化所有 Controller,绑定到生命周期
  3. Controller 内部:声明关心的信号 → 收到信号回调时刷对应 Preference 视图;用户操作时下发信号

信号与 UI 的连接全部在 Controller 里,Fragment 本身不碰信号。


2. 类继承体系

2.1 Controller 继承链

PreferenceController<V>                              (settingsBaseUi,所有 Controller 之祖)
   └─ CarPropertyMgrPreferenceController<V>          (settingsVehicleLib,★ 车控信号脊梁)
        ├─ BaseVehicleNewSwitchPrefController        (开关型,121 文件)
        ├─ BaseVehicleTabPreferenceController        (文字 Tab)
        ├─ BaseVehicleImageTabPreferenceController   (图标 Tab,与上"兄弟"非父子)
        ├─ BaseVehicleProgressPrefController         (SeekBar/进度)
        └─ BaseVehicleButtonGroupPropController      (按钮组)

⚠️ 三种业务基类是兄弟关系BaseVehicleImageTabPreferenceControllerBaseVehicleTabPreferenceController直接继承 CarPropertyMgrPreferenceControllerBaseVehicleImageTabPreferenceController.kt:25-26 注释明确说明),不是父子。

另有非 PreferenceController 体系的 Controller:

BaseCarPropertyController (Kotlin)    ← Fragment/Dialog 用,更轻量
   └─ FragmentVehicleController       ← Fragment 级集中订阅(如灯光右侧车模页)

2.2 Fragment 继承链

androidx.Fragment
└─ BaseFragment                       (最薄,非 Preference 页才用)
└─ micarx.preference.PreferenceFragment
   └─ BaseXmlParserSettingsFragment   (★ 核心:XML 解析 + Controller 管理 + ViewModel)
      ├─ SettingsFragment             (薄包装,仅 Title)
      │  └─ TopLevelSettingsFragment  (一级页:锚点跳转、EventBus、双栏)
      │     └─ EnergyManagerFragment / LightsSettingsFragment / ...
      └─ SecondLevelSettingsFragment  (二级页"车机管家",自带 Toolbar)
└─ BaseRightFragment                  (右侧车模图容器,独立链)

2.3 新建页面时该继承谁

场景继承
一级设置页(右侧车模 + Preference 列表 + 锚点)TopLevelSettingsFragment
二级设置页(“车机管家”风格,独立 Toolbar)SecondLevelSettingsFragment
普通 Preference 列表页SettingsFragment
非 Preference 自定义 View 页BaseFragment
单个开关设置项BaseVehicleNewSwitchPrefController
文字 Tab 设置项BaseVehicleTabPreferenceController
图标 Tab 设置项BaseVehicleImageTabPreferenceController
进度条设置项BaseVehicleProgressPrefController
Fragment 级集中订阅多个信号FragmentVehicleController(或 BaseCarPropertyController

3. 关键 Base 类

3.1 PreferenceController(根基类)

路径:base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceController.java

  • 实现 DefaultLifecycleObserveronCreateInternal / onStartInternal / ... / onDestroyInternal 自动派发
  • updateState(preference):单点刷新入口
  • handlePreferenceChanged / handlePreferenceClicked:用户事件
  • refreshUi()(L359-375)
  • getAvailabilityStatus():控制显隐/可用
  • requestDataFromHost / postDataToHost:走 Fragment 命令通道(Controller ↔ Fragment 通信)

3.2 CarPropertyMgrPreferenceController(车控脊梁)

路径:base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/common/CarPropertyMgrPreferenceController.java

  • 实现 CarPropertyManagerCallback(L56)
  • onCreateInternal(L511-518):new SettingsCarPropertyManager(ctx, settingsLifecycle),自动绑生命周期
  • 收到 CarService ready 后自动注册回调(onPropertyManagerPrepared L294 → registerPropertyCallback L269)

关键模板方法(子类可 override):

方法行号作用
getPropertyIdSet()L191(abstract)子类必填:声明下行/主信号 ID 集合
getObservedPropertyIdSet()L193默认 = getPropertyIdSet();要监听额外依赖信号时 override
getAreaId()L206多区域信号(车门、座椅)必须 override
getConfigPropertyRates()L198配置采样率
getSubDependPropertyId()L202父子项依赖
onHandlePropertyChange(value, shouldUpdateUI)L492子类刷 UI 的入口
shouldIgnorePropertyFeedback(propId, areaId)L688是否忽略这次回包
shouldUpdateUIByPropVal(value)L426预估值/无效值场景返回 false(UI 保持上一态,只置灰)
getCurrentIntermediateType(propId, propVal)L661中间态(loading)支持
obtainSetPropertyId()L651下行用 propId(下行/上行 ID 不一致时 override)
setVehicleProperty(propId, propVal, areaId, isSync)L620子类下发入口,已封装类型分发
restorePropertyToPrevious(propId, areaId)L125失败/超时回滚 UI(基类自动处理)

3.3 BaseVehicleNewSwitchPrefController(开关专用)

路径:base/settingsVehicleLib/.../miauto/vehicle/BaseVehicleNewSwitchPrefController.kt

开关类的”取反 + 写”被自动化了:

// L189-197  父类已实现
protected open fun setCarProperty(isChecked: Boolean) {
    if (needIntermediateState()) preference.isEnabled = false
    setVehicleProperty(obtainSetPropertyId(), getPropertyVal(isChecked), areaId)
    updatePreferencesVisibleWhenStatusChange(isChecked)
}
 
// L48-68  信号回传刷 UI
override fun onHandlePropertyChange(propertyValue, shouldUpdateUI) {
    // areaId 过滤后
    preference.setChecked(isPropertyOpen(propVal))
}

子类只需声明 getPropertyIdSet(),按需 override isPropertyOpen / getPropertyVal / needSecondConfirm / needIntermediateState / isValidPropVal

枚举型开关(如 LockBeep)重写 getPropertyVal(boolean)isPropertyOpen(int),见 LockBeepControllerBaseLockBeepControllerBaseVehicleNewSwitchPrefController 的”抽两层 Base”技巧。

3.4 BaseXmlParserSettingsFragment(Fragment 核心)

路径:base/settingsBaseUi/.../common/BaseXmlParserSettingsFragment.java

  • XML 单次解析生成 PreferenceController 列表(L301-322,反射 settings:controller 属性)
  • 生命周期分发给 Controller
  • getViewModel() / getDefaultViewModel()(L1406-1419)
  • DefaultViewModel 内置 ArrayMap<Class, MutableLiveData> 通用 LiveData 缓存(L1421-1449)——Controller 间通信的通用通道
  • 承载 BaseRightFragment 作为子 Fragment(L554-567)
  • use(Class, key) 取 Controller(L225-242)

4. 信号注册链路(Controller 创建时)

CarPropertyMgrPreferenceController.onCreateInternal (L511)
  └─► new SettingsCarPropertyManager(ctx, settingsLifecycle)         // 自动绑生命周期
onPostCreateInternal (L521)
  └─► mCarPropertyManager.setCarManagerCallback(this)                // 注册 service listener
[CarService ready 回调]
  └─► onPropertyManagerPrepared (L294)
       └─► registerPropertyCallback() (L269)
            └─► mCarPropertyManager.registerPropertyCallback(getObservedPropertyIdSet())
                 └─► VehicleControlManager.registerPropertyCallback
                      └─► CarPropertyCacheManager.registerPropertyCallback   // 首次才向 server 注册

⚠️ 易错点registerPropertyCallback 必须在 onPropertyManagerPrepared 之后调,否则服务没连上注册失败。基类已处理好,子类不要自己调。


5. 信号回传链路(车端 → UI)

车端 ECU → CarService → AOSP CarPropertyManager
  └─► CarPropertyCacheManager.mInternalCarPropertyEventCallback.onChangeEvent (L61)
       ├─► mPropertiesCache.put(propId#areaId, value)                 // 全局缓存
       └─► 遍历 mCallBackMap[propId](去重后的 dispatcher)
            └─► SettingsCarPropertyEventCallback.onChangeEvent (L662)
                 ├─► cachePropVal                                     // per-instance 缓存
                 ├─► processTimeout                                   // 终止/重启超时
                 └─► mCallback.onPropertyChanged(value)               // 命中观察集才回调
                      └─► CarPropertyMgrPreferenceController.onPropertyChanged (L346)
                           ├─► shouldIgnorePropertyFeedback / shouldUpdateUIByPropVal 过滤
                           ├─► IVehiclePerformFeature.needMapReceiveDependProperty → 依赖信号映射 (L360-380)
                           └─► onHandlePropertyChange(value, shouldUpdateUI) (L492)
                                └─► 子类刷新具体 UI(setChecked / setTab / setLoading…)

关键:车辆 Controller 的刷新逻辑在 onHandlePropertyChange 里,不是updateState 里。


6. 信号回调接口:CarPropertyManagerCallback

路径:base/settingsVehicleLib/.../miauto/vehicle/CarPropertyManagerCallback.java(全 default 方法)

方法触发时机
onPropertyManagerPrepared()CarService 连接就绪
onPropertyManagerReleased()CarService 断开
onPropertyChanged(value)信号变化(仅 observedSet 内的 propId)
onSetPropertyTimeout(propId, areaId)下发超时未回弹
onSetPropertyError(propId, areaId)底层 onErrorEvent
onDataVerifyFailed(propId, areaId, val)下发值 ≠ 回弹缓存值
getTimeoutFlag(value)CONTINUE / RESTART / REMOVE
isIgnoreCache(value)该值是否跳过缓存
reportOneTrackWhenSetProperty(...)下发埋点

⚠️ 常见坑:如果业务忘了把 propId registerPropertyCallback 进 observedSet,即使底层有事件,onPropertyChanged不会回调,只打 w 级警告 "un-excepted observe propId"


7. UI 刷新动作

onHandlePropertyChange 里直接操作 Preference 视图:

// 开关勾选
preference.setChecked(isPropertyOpen(propVal))
 
// 中间加载态(需 needIntermediateState()=true)
preference.setLoading(isInLoadingState(value))
 
// 可用性(不走原生 AVAILABLE_FOR_VIEWING)
preference.isEnabled = isPropertyAvailable(...) && isInnerLogicAvailable(...)
  • areaId 过滤onHandlePropertyChange 第一步检查 areaId,避免一个信号触发多个开关
  • 失败回滚onSetPropertyTimeout / onSetPropertyErrorrestorePropertyToPrevious(基类自动,子类无需关心)
  • 配置字驱动可见性:Fragment 的 getPreferenceKeyResIdsToRemove() 按车型/配置字移除整个 Preference;Controller 的 getAvailabilityStatus() 处理静态判断
  • 缓存兜底VehiclePropertyConfig.getPropValueForFirstCache(value) 让 UI 首次拿到终态值避免抖动(前提是首次缓存前有 getProperty 过)

8. Feature 位接口:IVehiclePerformFeature

路径:base/settingsVehicleLib/.../com/android/micar/settings/carproperty/IVehiclePerformFeature.java(注意包名差异)

用位标志挂载可复用功能,避免重复造轮子:

int FEATURE_NONE = 0;
int FEATURE_REFRESH_SUMMARY_ON_RESUME = 1 << 1;
// 后续 1 << n 扩展

Controller extends ... implements IVehiclePerformFeature,重写:

  • needMapReceiveDependProperty():把依赖信号(错误码)映射成主信号刷新
  • featureShouldDoubleConfirm(boolean):二次确认
  • featureDisableReason():禁用原因
  • featureIgnorePropertyValues(propId):忽略的信号值

范例:CreepSpeedPreferenceControllersettingsPage/micarDrivingSettings/.../CreepSpeedPreferenceController.java:24

@Override
public ArraySet<Integer> getPropertyIdSet() {
    return new ArraySet<>(Set.of(MiCarPropertyIds.DrivingCtrl.CREEP_SPEED_STATUS));   // 主信号
}
@Override
public ArraySet<Integer> getObservedPropertyIdSet() {
    ArraySet<Integer> set = new ArraySet<>(getPropertyIdSet());
    set.add(MiCarPropertyIds.DrivingCtrl.CREEP_SPEED_ERROR_REASON);   // 追加依赖信号
    return set;
}
@Override
public boolean needMapReceiveDependProperty() { return true; }   // 基类自动映射

9. 车模联动

点击设置项时同步切换右侧车模图:

CarPropertyMgrPreferenceController.java:743-768 changeCarModelWhenClick

getDefaultViewModel()
    .getLiveData(CarModelData.class)
    .setValue(new CarModelData(...));   // 通过 DefaultViewModel 通用 LiveData 通道推给右侧 BaseRightFragment

CarModelData 路径:base/settingsBaseUi/.../common/livedata/CarModelData.kt


10. 范例:最小开关 Controller(26 行)

settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/lock/WalkAwayLockController.kt

class WalkAwayLockController(
    context: Context, preferenceKey: String,
    fragmentController: FragmentController, uxRestrictions: CarUxRestrictions
) : BaseVehicleNewSwitchPrefController(context, preferenceKey, fragmentController, uxRestrictions) {
 
    override fun getPropertyIdSet(): ArraySet<Int> = ArraySet<Int>().apply {
        add(MiCarPropertyIds.LockCtrl.WALK_AWAY_LOCK)   // 唯一必填
    }
}

基类已做完:注册 callback、缓存、超时、回滚、UI 刷新、二次确认。


11. 范例:Fragment 级集中订阅(灯光车模页)

settingsPage/micarLightSettings/.../LightsRightFragmentController.kt 继承 FragmentVehicleController

  • 集中订阅一束灯光状态信号(POSITION_BEAM_STATUSHIGH_BEAM_STATUSMOOD_LIGHT_TRIGGER 等)
  • onPropertyChanged 里驱动右侧车模图片切换

这种模式适合”一个页面整体联动”,而非单个 Preference。


12. 关键路径速查

主题路径
Controller 根基类base/settingsBaseUi/.../common/PreferenceController.java
车控脊梁base/settingsVehicleLib/.../miauto/common/CarPropertyMgrPreferenceController.java
开关基类base/settingsVehicleLib/.../miauto/vehicle/BaseVehicleNewSwitchPrefController.kt
文字 Tab 基类base/settingsVehicleLib/.../miauto/vehicle/BaseVehicleTabPreferenceController.kt
图标 Tab 基类base/settingsVehicleLib/.../miauto/vehicle/BaseVehicleImageTabPreferenceController.kt
Fragment 级基类base/settingsVehicleLib/.../miauto/common/BaseCarPropertyController.kt
FragmentVehicleControllerbase/settingsVehicleLib/.../com/android/micar/settings/common/FragmentVehicleController.java
Fragment 核心base/settingsBaseUi/.../common/BaseXmlParserSettingsFragment.java
一级页基类base/settingsBaseUi/.../miauto/TopLevelSettingsFragment.java
Feature 位接口base/settingsVehicleLib/.../com/android/micar/settings/carproperty/IVehiclePerformFeature.java
最小开关范例settingsPage/micarLockSettings/.../lock/WalkAwayLockController.kt
依赖信号范例settingsPage/micarDrivingSettings/.../driving/CreepSpeedPreferenceController.java

新增 Controller 的完整步骤见 09 开发模板