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 效果 | 生命周期回调 |
|---|---|---|---|
AVAILABLE | 0 | 可见可点 | 全回调 |
CONDITIONALLY_UNAVAILABLE | 1 | 隐藏 | — |
UNSUPPORTED_ON_DEVICE | 2 | 彻底隐藏不占位 | 所有生命周期都不回调 |
DISABLED_FOR_PROFILE | 3 | 隐藏 | — |
AVAILABLE_FOR_VIEWING | 4 | 可见但禁用(只读) | — |
生命周期方法(子类重写钩子)
| final 入口 | Internal 钩子 | 用途 |
|---|---|---|
onCreate | onCreateInternal / onPostCreateInternal | 加载配置;基类随后 refreshUi() |
onStart | onStartInternal | 注册监听器(随后 refreshUi()) |
onResume | onResumeInternal | 瞬态刷新 |
onStop | onStopInternal | 注销监听器 |
onDestroy | onDestroyInternal | 释放资源 |
onUxRestrictionsChanged | onApplyUxRestrictions | 行驶状态锁定 |
业务回调(最常用)
updateState(V preference)— 刷新 UI(必须幂等,每次refreshUi()调)handlePreferenceChanged(V, Object)— 用户改开关/滑块handlePreferenceClicked(V)— 用户点击getAvailabilityStatus()— 控制可见/禁用
四、Controller 继承体系
A. 通用 UI 基类(settingsBaseUi/…/miauto/preferences/)
| 基类 | Preference 类型 | 场景 |
|---|---|---|
BaseSwitchPreferenceController.kt | MiCarNewSwitchPreference | 开关(带 PreCheckInterceptor 切换前拦截) |
BaseTabPreferenceController.kt | NewTabPreference | 分段标签(关/自动/开) |
BaseButtonTogglePreferenceController.java | ButtonTogglePreference | 图标按钮组 |
BaseUrlController.java | — | WebView/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)
关键:无需手写 setOnClickListener,setPreference() 自动连接。
use() 机制:传运行时参数
// 在 Fragment.onAttach 中,向 XML 反射构建好的 Controller 传参
use(UrlController.class, R.string.mi_ci_car_net_service).setOriginUrl(...);查 mPreferenceControllersLookup map(key=Class, value=List)。Javadoc 提醒「明智使用,最小化耦合」。
六、三个「螺钉」接口
| 接口 | 实现者 | 作用 |
|---|---|---|
FragmentHost | Activity | Fragment 请求 Activity:launchFragment / goBack |
UxRestrictionsProvider | Activity | 给 Fragment 查驾驶限制 |
FragmentController | Fragment(BaseXmlParserSettingsFragment 实现) | Controller 反向操作 Fragment:launchFragment / goBack / showDialog / findPreferenceByKey / findControllerByKey / onHandleCmd / getViewModel |
七、一个完整最小例子(车辆开关)
以「夜间氛围灯减弱」为例,只需 30 行 Controller:
XML(miauto_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 父子展开、驾驶限制锁定。
八、新增设置页的约束清单(避坑)
- Controller 必须有
(Context, String, FragmentController, CarUxRestrictions)构造器(反射要求)。 - Controller 必须重写
getPreferenceType()返回匹配 XML 的 Preference 类。 - XML 节点必须有
android:key(且是@string/pk_*,禁硬编码)+settings:controller(全限定类名)。 - 自定义 Preference 类名必须以
Preference/PreferenceGroup/PreferenceCategory结尾,否则PreferenceXmlParser会跳过(项目里标注的已知坑)。 - 车型差异:通过
getPreferenceKeyResIdsToRemove()返回R.string.pk_*列表移除,或getAvailabilityStatus()返回UNSUPPORTED_ON_DEVICE。 @RouterProvider的 path 改了必须重新 build(kapt 编译期生成)。- 新增模块必须在
settings.gradle加include且在SettingsApplication.@Modules数组加模块名(漏了路由静默失效)。
完整的新增页 10 步流程见现有
docs/08-开发实战-新增设置页.md。