03 · PreferenceController 范式

这是整个项目的核心开发范式。理解它,就理解了 90% 的设置页是怎么写的。 所有设置页 = 一个 Fragment(声明 XML) + N 个 PreferenceController(每个 Preference 一个)

一、为什么不是 AndroidX Preference

项目用的是自定义 fork micarx.preference.*(小米定制的 Preference 库),底层继承自 AOSP Car UI 的 com.android.car.ui.mi.preference.PreferenceFragment,而 androidx.preference。这是为了适配车机交互(DPAD 焦点、驾驶限制、语音热词)。

二、Fragment 继承体系(源码核实)

⚠️ 此处修正了 Agent 转述:BaseXmlParserSettingsFragment核心父类,不是子类。

com.android.car.ui.mi.preference.PreferenceFragment  (AOSP Car UI fork)
        ▲
        │ extends
        │
BaseXmlParserSettingsFragment   (1562 行, abstract, 包含全部装配逻辑)
   implements FragmentController, DialogCreatable, OnUxRestrictionsChangedListener
   │
   ├── SettingsFragment              (57 行, abstract, 轻量; 仅工具栏助手)
   │      │
   │      └── TopLevelSettingsFragment  (760 行, 一级页面基类)
   │             ├── EnergyManagerFragment          (充电)
   │             ├── LightsSettingsFragment         (灯光)
   │             ├── UniversalSettingsFragment      (系统)
   │             └── ... (各一级页)
   │
   └── SecondLevelSettingsFragment   (204 行, 二级页面基类, 带返回工具栏)

核心结论:写新一级页 → 继承 TopLevelSettingsFragment;写新二级页 → 继承 SecondLevelSettingsFragment。两者最终都继承自 BaseXmlParserSettingsFragment(所有逻辑在这里)。

三、PreferenceController 根基类

文件base/settingsBaseUi/.../common/PreferenceController.java(857 行)

public abstract class PreferenceController<V extends Preference> implements
        DefaultLifecycleObserver,                       // 生命周期自动绑定
        OnUxRestrictionsChangedListener {               // 驾驶限制回调
    // 构造器签名固定(反射强约束!)
    public PreferenceController(Context context, String preferenceKey,
                                FragmentController fragmentController,
                                CarUxRestrictions uxRestrictions) { ... }
 
    protected abstract Class<V> getPreferenceType();    // 唯一抽象方法
}

关键设计

  • 泛型 <V extends Preference>:绑定 Controller 操作的 Preference 类型。setPreference() 类型不匹配会抛 IllegalArgumentException
  • 构造器必须 4 参 (Context, String, FragmentController, CarUxRestrictions)PreferenceControllerListHelper.createInstance 用反射按此签名实例化,少了或多了都会 NoSuchMethodException 崩溃。
  • 模板方法模式:外部生命周期入口是 final,子类重写 ...Internal() 钩子。

AvailabilityStatus 五态(控制可见/禁用)

常量UI 效果生命周期回调
AVAILABLE0可见可点全回调
CONDITIONALLY_UNAVAILABLE1隐藏
UNSUPPORTED_ON_DEVICE2彻底隐藏不占位所有生命周期都不回调
DISABLED_FOR_PROFILE3隐藏
AVAILABLE_FOR_VIEWING4可见但禁用(只读)

生命周期方法(子类重写钩子)

final 入口Internal 钩子用途
onCreateonCreateInternal / onPostCreateInternal加载配置;基类随后 refreshUi()
onStartonStartInternal注册监听器(随后 refreshUi()
onResumeonResumeInternal瞬态刷新
onStoponStopInternal注销监听器
onDestroyonDestroyInternal释放资源
onUxRestrictionsChangedonApplyUxRestrictions行驶状态锁定

业务回调(最常用)

  • updateState(V preference) — 刷新 UI(必须幂等,每次 refreshUi() 调)
  • handlePreferenceChanged(V, Object) — 用户改开关/滑块
  • handlePreferenceClicked(V) — 用户点击
  • getAvailabilityStatus() — 控制可见/禁用

四、Controller 继承体系

A. 通用 UI 基类(settingsBaseUi/…/miauto/preferences/)

基类Preference 类型场景
BaseSwitchPreferenceController.ktMiCarNewSwitchPreference开关(带 PreCheckInterceptor 切换前拦截)
BaseTabPreferenceController.ktNewTabPreference分段标签(关/自动/开)
BaseButtonTogglePreferenceController.javaButtonTogglePreference图标按钮组
BaseUrlController.javaWebView/URL 页

B. 车辆属性基类(settingsVehicleLib/…/miauto/)

PreferenceController<V>                        (settingsBaseUi)
    ▲
    │ extends
CarPropertyMgrPreferenceController<V>          (settingsVehicleLib, 持有 SettingsCarPropertyManager)
    ▲
    │ extends
    ├── BaseVehicleNewSwitchPrefController.kt       (开关, 最常用)
    ├── BaseVehicleTabPreferenceController.kt       (文字 Tab)
    ├── BaseVehicleImageTabPreferenceController.kt  (图标 Tab, 与前者并行非父子)
    ├── BaseVehicleProgressPrefController.java      (进度/滑块)
    ├── BaseVehicleButtonGroupPropController.java
    ├── BaseVehicleButtonToggleGroupPropController.kt
    ├── BaseVehicleButtonIconToggleGroupPropController.kt
    └── BaseVehiclePropertyDialogController.kt      (对话框)

另有轻量 BaseCarPropertyController.kt(不绑 Preference,纯逻辑,用于非 Preference 屏幕)。

五、Fragment ↔ Controller 装配链路

发生在 BaseXmlParserSettingsFragment

onAttach(Context)
  │
  ├─ PreferenceXmlParser.extractMetadata(xml)   // 单次解析 XML (key/controller/dependKey/hiddenFeatures)
  │      flags: FLAG_NEED_KEY | FLAG_NEED_PREF_CONTROLLER | FLAG_NEED_DEPEND_KEY | FLAG_NEED_HIDDE_FEATURES
  │
  ├─ PreferenceControllerListHelper.getPreferenceControllersFromMetadata(...)
  │      └─ createInstance: 反射 Class.forName + getConstructor(4参).newInstance
  │             └─ Router.getInstance().holdController(controller, hostFragment)  // 给路由器登记实例
  │
  ├─ lifecycle.addObserver(controller)           // 生命周期自动绑定
  └─ mPreferenceControllersLookup[controllerClass].add(controller)  // 按 Class 索引

onCreatePreferences(...)
  │
  ├─ addPreferencesFromResource(resId)
  └─ for each controller:
        ├─ controller.setDependKeyList(...)
        └─ controller.setPreference(screen.findPreference(key))
               └─ 类型校验 + 自动注册 OnPreferenceChangeListener/ClickListener
                     (→ 自动路由到 handlePreferenceChanged / handlePreferenceClicked)

关键:无需手写 setOnClickListenersetPreference() 自动连接。

use() 机制:传运行时参数

// 在 Fragment.onAttach 中,向 XML 反射构建好的 Controller 传参
use(UrlController.class, R.string.mi_ci_car_net_service).setOriginUrl(...);

mPreferenceControllersLookup map(key=Class, value=List)。Javadoc 提醒「明智使用,最小化耦合」。

六、三个「螺钉」接口

接口实现者作用
FragmentHostActivityFragment 请求 Activity:launchFragment / goBack
UxRestrictionsProviderActivity给 Fragment 查驾驶限制
FragmentControllerFragment(BaseXmlParserSettingsFragment 实现)Controller 反向操作 Fragment:launchFragment / goBack / showDialog / findPreferenceByKey / findControllerByKey / onHandleCmd / getViewModel

七、一个完整最小例子(车辆开关)

以「夜间氛围灯减弱」为例,只需 30 行 Controller

XMLmiauto_lights_settings_fragment.xml):

<com.android.car.settings.miauto.preferences.MiCarNewSwitchPreference
    android:key="@string/pk_settings_ambient_light_dimming_down_entry"
    android:title="@string/settings_ambient_light_dimming_down_title"
    settings:controller="com.android.micar.settings.display.AmbientLightDimmingDownSwitchPrefController"
    settings:dependKey="@string/pk_settings_display_atmosphere_lights_entry" />

Controller(30 行):

public class AmbientLightDimmingDownSwitchPrefController
        extends BaseVehicleNewSwitchPrefController implements IVehiclePerformFeature {
 
    public AmbientLightDimmingDownSwitchPrefController(Context ctx, String key,
            FragmentController fc, CarUxRestrictions ux) { super(ctx, key, fc, ux); }
 
    @Override protected ArraySet<Integer> getPropertyIdSet() {
        ArraySet<Integer> ids = new ArraySet<>();
        ids.add(MiCarPropertyIds.LightCtrl.AMBIENT_LIGHT_DIMMING_DOWN_SWITCH);  // 绑定信号
        return ids;
    }
    @Override public boolean isPropertyOpen(int propVal) { return propVal == SwitchWithDefault.ON; }
    @Override protected int getPropertyVal(boolean isChecked) {
        return isChecked ? SwitchWithDefault.ON : SwitchWithDefault.OFF;
    }
}

基类免费提供:开关读写、信号回流刷新、二次确认、loading 中间态、超时回滚、埋点、dependKey 父子展开、驾驶限制锁定。

八、新增设置页的约束清单(避坑)

  1. Controller 必须(Context, String, FragmentController, CarUxRestrictions) 构造器(反射要求)。
  2. Controller 必须重写 getPreferenceType() 返回匹配 XML 的 Preference 类。
  3. XML 节点必须android:key(且是 @string/pk_*,禁硬编码)+ settings:controller(全限定类名)。
  4. 自定义 Preference 类名必须Preference/PreferenceGroup/PreferenceCategory 结尾,否则 PreferenceXmlParser 会跳过(项目里标注的已知坑)。
  5. 车型差异:通过 getPreferenceKeyResIdsToRemove() 返回 R.string.pk_* 列表移除,或 getAvailabilityStatus() 返回 UNSUPPORTED_ON_DEVICE
  6. @RouterProvider 的 path 改了必须重新 build(kapt 编译期生成)。
  7. 新增模块必须settings.gradleinclude SettingsApplication.@Modules 数组加模块名(漏了路由静默失效)。

完整的新增页 10 步流程见现有 docs/08-开发实战-新增设置页.md