02 - 核心架构:PreferenceController 范式
本篇概览
MiCarSettings 沿用了 AOSP Car Settings 的核心架构:PreferenceFragment + PreferenceController 组合模式。这个范式决定了你以后在这个项目里 90% 的业务怎么写、bug 怎么查、功能怎么加。
本篇聚焦三个问题:
PreferenceController是什么?它和Fragment、Activity怎么协作?- 一个写在 XML 里的
settings:controller="xxx"字符串,是怎么变成一个跑在内存里、能收信号能刷 UI 的对象的? - 为什么项目里”加一个开关”的标准动作是 新建一个
PreferenceController子类 + 在 XML 里注册一行,而不是去改Fragment?
读完本篇你能掌握:
- 看懂任何一门设置业务页面(灯光、充电、锁车……)的代码骨架
- 知道一个
Controller从onCreateInternal()到onDestroyInternal()的完整生命周期,以及它和Activity/Fragment生命周期的对应关系 - 理解
AvailabilityStatus五种状态对 UI 的实际影响(什么时候会被隐藏、什么时候会被禁用) - 能独立完成”新增一个设置项”的全套改动:写
Controller、声明 XML、配置key - 知道
BaseCarSettingsActivity→BaseXmlParserSettingsFragment→PreferenceController这条数据通路,定位问题能从 UI 反推到信号,再从信号反推到Controller
一、为什么需要 PreferenceController?
先看一下原始 AOSP PreferenceFragment 的写法:所有业务逻辑都堆在 Fragment 里——找 Preference、设 title、注册 ClickListener、订阅信号、刷新 UI……一个页面几十个开关,Fragment 就会膨胀到上千行,逻辑互相耦合,根本没法多人协作。
AOSP Car Settings 的解法是**“把每个 Preference 的业务逻辑拆成一个独立的 Controller”**:
- 一个
Controller只管一个Preference(一组开关、一个滑块、一个 Tab) Controller自己有完整的生命周期(onCreate/onStart/onResume/onStop/onDestroy),是一个最小业务单元Fragment只负责把页面上的所有Controller串起来,不再写具体业务Controller通过 XML 声明,框架用反射帮你实例化好
核心思想:
Fragment是”页面骨架”,Controller是”肌肉”。骨架只负责装配,肌肉负责动。
📌 新手记住:在 MiCarSettings 里写新功能,绝大多数情况下你只需要写一个 Controller 子类,不需要改 Fragment。Fragment 是公共骨架,改它会影响所有人。
二、Activity–Fragment–Controller 三层关系
2.1 三层各自的职责
classDiagram direction TB class BaseCarSettingsActivity { -CarUxRestrictions mCarUxRestrictions -ViewGroup mFragmentContainer +launchFragment(Fragment) +goBack() +getCarUxRestrictions() CarUxRestrictions +onUxRestrictionsChanged(CarUxRestrictions) } class FragmentHost { <<interface>> +launchFragment(Fragment) +goBack() +showBlockingMessage() } class UxRestrictionsProvider { <<interface>> +getCarUxRestrictions() CarUxRestrictions } class SubSettingsActivity { +newInstance(Context, Fragment) Intent +getInitialFragment() Fragment } class BaseFragment { <<abstract>> +getFragmentHost() FragmentHost +getCurrentRestrictions() CarUxRestrictions #getLayoutId() int } class BaseXmlParserSettingsFragment { <<abstract>> -List~PreferenceController~ mPreferenceControllers -Map mPreferenceControllersLookup +getPreferenceScreenResId() int +onAttach(Context) +onCreatePreferences(Bundle, String) +findControllerByKey(String) PreferenceController +use(Class, int) T } class SettingsFragment { <<abstract>> } class PreferenceController~V~ { <<abstract>> #Context mContext #String mPreferenceKey #FragmentController mFragmentController #V mPreference +getPreferenceType() Class~V~* +getAvailabilityStatus() int +onCreateInternal() +onStartInternal() +updateState(V) +handlePreferenceChanged(V, Object) } BaseCarSettingsActivity ..|> FragmentHost BaseCarSettingsActivity ..|> UxRestrictionsProvider SubSettingsActivity --|> BaseCarSettingsActivity BaseXmlParserSettingsFragment --|> BaseFragment SettingsFragment --|> BaseXmlParserSettingsFragment BaseXmlParserSettingsFragment o-- PreferenceController : 持有 N 个 BaseCarSettingsActivity o-- BaseXmlParserSettingsFragment : 承载 PreferenceController ..> BaseXmlParserSettingsFragment : 通过 FragmentController 接口回调
图里的箭头含义:
--|>:继承..|>:实现接口o--:持有(组合关系)..>:依赖
关键关系链:
Activity实现FragmentHost和UxRestrictionsProvider接口 —— 这是Fragment能挂载到Activity上的硬性约束(见BaseFragment.java:119-124的强校验)。BaseXmlParserSettingsFragment实现了FragmentController接口 ——Controller不直接持有Fragment,而是通过FragmentController这个接口反向操作(launchFragment、goBack、showDialog、findPreferenceByKey等)。SettingsFragment继承BaseXmlParserSettingsFragment—— 它本身只是个壳,业务页面再继承SettingsFragment。
📌 新手记住:记住三个接口名——FragmentHost(Activity 实现,给 Fragment 用)、UxRestrictionsProvider(Activity 实现,给 Fragment 查驾驶限制)、FragmentController(Fragment 实现,给 Controller 用)。这三者就是整个架构的”螺钉”。
2.2 一个真实页面:灯光设置页
以灯光设置页为例,它的继承链是:
LightsSettingsFragment
└── AudioControlSettingsFragment
└── TopLevelSettingsFragment
└── SettingsFragment
└── BaseXmlParserSettingsFragment ← Controller 装配真正发生在这里
└── BaseFragment
LightsSettingsFragment 本身只有 145 行(settingsPage/micarLightSettings/src/main/java/com/android/car/settings/miauto/lights/LightsSettingsFragment.java),核心只做了三件事:
// 文件: settingsPage/micarLightSettings/.../LightsSettingsFragment.java:67-70
@Override
@XmlRes
public int getPreferenceScreenResId() {
return R.xml.miauto_lights_settings_fragment; // 1. 指定页面 XML
}
// 文件: settingsPage/micarLightSettings/.../LightsSettingsFragment.java:73-120
@Override
public List<Integer> getPreferenceKeyResIdsToRemove() {
// 2. 根据车型/License 决定隐藏哪些 Preference(不同车型配置不同功能)
...
}
// 文件: settingsPage/micarLightSettings/.../LightsSettingsFragment.java:122-125
@Override
public BaseRightFragment getRightFragment() {
return new LightsRightFragment(); // 3. 配置右侧面板(双屏车型才有)
}真正干活的 20+ 个 Controller(外灯、雾灯、大灯延时、氛围灯……)全部在 XML 里声明,Fragment 一个都没提。这就是这个范式的威力。
📌 新手记住:写新页面时,Fragment 类应该尽量”瘦”。所有”这个开关点击后做什么、这个标题显示什么、这个开关要不要隐藏”的判断,统统丢给 Controller。Fragment 只决定”页面有哪些项、各项怎么排布”。
三、PreferenceController 抽象基类详解
文件:base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceController.java
这是整个范式的核心,必须读源码。
3.1 类签名与泛型
// 文件: base/settingsBaseUi/.../common/PreferenceController.java:106-108
public abstract class PreferenceController<V extends Preference> implements
DefaultLifecycleObserver,
OnUxRestrictionsChangedListener {两个关键点:
- 泛型
<V extends Preference>:子类必须声明自己管的是哪种子Preference(如MiCarNewSwitchPreference、NewTabPreference、SliderPreference)。框架在setPreference()时会做类型校验,类型对不上直接抛IllegalArgumentException(PreferenceController.java:329-338)。 - 实现
DefaultLifecycleObserver:这意味着Controller是一个生命周期感知组件。Fragment把它注册成Lifecycle观察者后,onCreate/onStart/onResume/onPause/onStop/onDestroy会自动派发过来。
flowchart TD A["Fragment.onAttach"] --> B["解析 XML,反射 new 出所有 Controller"] B --> C["lifecycle.addObserver(controller)"] C --> D["Fragment.onCreatePreferences<br/>遍历 Controller,setPreference(pref)"] D --> E["Controller.setPreference:<br/>类型校验 + 注册点击/变更监听"] subgraph 生命周期派发 F["Fragment.onCreate"] --> G["Controller.onCreate:<br/>检查 AvailabilityStatus<br/>onCreateInternal<br/>refreshUi"] H["Fragment.onStart"] --> I["Controller.onStart:<br/>onStartInternal<br/>refreshUi"] J["Fragment.onResume"] --> K["Controller.onResume:<br/>onResumeInternal"] L["Fragment.onPause"] --> M["Controller.onPause:<br/>onPauseInternal"] N["Fragment.onStop"] --> O["Controller.onStop:<br/>onStopInternal"] P["Fragment.onDestroy"] --> Q["Controller.onDestroy:<br/>onDestroyInternal<br/>dismissAutoDismissDialog"] end R["Fragment.onDetach"] --> S["lifecycle.removeObserver(controller)"]
3.2 必须实现的方法:getPreferenceType()
// 文件: base/settingsBaseUi/.../common/PreferenceController.java:506-507
protected abstract Class<V> getPreferenceType();这是唯一一个子类必须实现的抽象方法。返回你管的 Preference 的具体类型:
// 文件: base/settingsBaseUi/.../miauto/preferences/BaseSwitchPreferenceController.kt:40-42
override fun getPreferenceType(): Class<MiCarNewSwitchPreference> {
return MiCarNewSwitchPreference::class.java
}3.3 AvailabilityStatus:可用性状态
PreferenceController 用一个 @IntDef 注解定义了五种可用性状态:
// 文件: base/settingsBaseUi/.../common/PreferenceController.java:117-148
@Retention(RetentionPolicy.SOURCE)
@IntDef({AVAILABLE, CONDITIONALLY_UNAVAILABLE, UNSUPPORTED_ON_DEVICE,
DISABLED_FOR_PROFILE, AVAILABLE_FOR_VIEWING})
public @interface AvailabilityStatus {
}
public static final int AVAILABLE = 0; // 可见且可操作(默认)
public static final int CONDITIONALLY_UNAVAILABLE = 1; // 当前不可用,将来可能可用 → 隐藏
public static final int UNSUPPORTED_ON_DEVICE = 2; // 设备根本不支持 → 隐藏且不调任何生命周期
public static final int DISABLED_FOR_PROFILE = 3; // 当前用户无权修改 → 隐藏
public static final int AVAILABLE_FOR_VIEWING = 4; // 可见但禁用(只读展示)这五种状态决定了 UI 最终如何展示。refreshUi() 里的判断逻辑(精简版):
// 文件: base/settingsBaseUi/.../common/PreferenceController.java:359-375
public final void refreshUi() {
if (!mIsCreated) return;
if (isAvailable()) { // AVAILABLE 或 AVAILABLE_FOR_VIEWING
if (defaultVisibleToUserWhenRefreshUi()) {
mPreference.setVisible(true);
}
updateState(mPreference); // 让子类刷新 UI
onApplyUxRestrictions(mUxRestrictions); // 应用驾驶限制
} else {
mPreference.setVisible(false); // 其他三种状态 → 隐藏
}
}📌 新手记住:改 getAvailabilityStatus() 的返回值是控制”隐藏/显示”最干净的做法。比如某个功能在某些车型上没有,重写它返回 UNSUPPORTED_ON_DEVICE,整个 Controller 的生命周期都不会被触发,省电省心。注意:onCreate/onStart/... 全系列方法在 UNSUPPORTED_ON_DEVICE 状态下都不会被调用(PreferenceController.java:402-407)。
3.4 状态机
stateDiagram-v2 [*] --> Instantiated: XML 解析反射 new Instantiated --> UNSUPPORTED_ON_DEVICE: getAvailabilityStatus()==2 Instantiated --> CheckAvailable: getAvailabilityStatus()!=2 UNSupported_ON_DEVICE --> [*]: 不调任何生命周期<br/>Preference.setVisible(false) CheckAvailable --> Hidden: CONDITIONALLY_UNAVAILABLE(1)<br/>DISABLED_FOR_PROFILE(3) CheckAvailable --> Visible: AVAILABLE(0) Visible --> ViewOnly: AVAILABLE_FOR_VIEWING(4)<br/>onApplyUxRestrictions 会禁用 Visible --> Editable: AVAILABLE(0) Hidden --> Visible: refreshUi()<br/>状态变化 Visible --> Hidden: refreshUi()<br/>状态变化 Editable --> Disabled: 驾驶状态变化<br/>UX_RESTRICTIONS_NO_SETUP Disabled --> Editable: 驾驶状态恢复 note right of UNSUPPORTED_ON_DEVICE 最彻底:完全屏蔽 onCreate/onStart 都不进 end note note right of ViewOnly 常用于"只读展示" 点击不响应 end note
3.5 子类可重写的回调方法
按生命周期顺序列出常用回调(全部定义在 PreferenceController.java:567-624):
| 回调方法 | 触发时机 | 典型用途 |
|---|---|---|
onCreateInternal() | Fragment.onCreate | 初始化数据、设置默认值、读配置 |
onStartInternal() | Fragment.onStart | 注册广播/信号监听 |
onResumeInternal() | Fragment.onResume | 重新拉取数据刷新 UI(页面回来后) |
onPauseInternal() | Fragment.onPause | 暂停一些 UI 动画 |
onStopInternal() | Fragment.onStop | 反注册监听(重要,不然内存泄漏) |
onDestroyInternal() | Fragment.onDestroy | 释放资源 |
updateState(V preference) | refreshUi() 被调用时 | 根据信号/状态刷新 UI 显示(最常用) |
handlePreferenceChanged(V, Object) | 用户改变开关/滑块时 | 处理用户操作(最常用) |
handlePreferenceClicked(V) | 用户点击 Preference 时 | 跳页面、弹对话框 |
getAvailabilityStatus() | 生命周期开始前 + refreshUi() | 控制可见/禁用 |
onApplyUxRestrictions(CarUxRestrictions) | 驾驶状态变化 | 自定义驾驶限制 |
📌 新手记住:onStartInternal() 里订阅了什么,onStopInternal() 里就要反订阅什么。这条规则记牢,能避开 80% 的内存泄漏。updateState() 必须是幂等的——它会被反复调用,不要在里面做”一次性初始化”。
四、PreferenceControllerListHelper:XML → 对象实例化
文件:base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceControllerListHelper.java
这个类只有两个公开静态方法,但它是整个范式的”装配车间”。
4.1 核心流程
flowchart LR A["Fragment.onAttach"] --> B["PreferenceXmlParser.extractMetadata<br/>解析 XML,提取 Bundle 列表"] B --> C["每个 Bundle 包含:<br/>· key<br/>· controller 全限定类名<br/>· dependKey"] C --> D["buildControllers:<br/>遍历 Bundle"] D --> E["createInstance:<br/>Class.forName(controllerName)"] E --> F["反射拿 4 参数构造器:<br/>(Context, String, FragmentController, CarUxRestrictions)"] F --> G["newInstance 创建 Controller"] G --> H["Router.getInstance().holdController<br/>注册到全局路由表(供外部跳转)"] H --> I["controller.setDependKey(dependKey)"] I --> J["返回 List~PreferenceController~"]
4.2 关键源码
// 文件: base/settingsBaseUi/.../common/PreferenceControllerListHelper.java:110-129
private static PreferenceController createInstance(String controllerName,
Context context, String key, String dependKey,
FragmentController fragmentController,
CarUxRestrictions restrictionInfo) {
try {
Class<?> clazz = Class.forName(controllerName); // 1. 按名字加载类
// 2. 必须有这个固定签名的构造器
Constructor<?> preferenceConstructor = clazz.getConstructor(
Context.class, String.class,
FragmentController.class, CarUxRestrictions.class);
Object[] params = new Object[]{context, key, fragmentController, restrictionInfo};
// 3. 反射实例化
PreferenceController preferenceController =
(PreferenceController) preferenceConstructor.newInstance(params);
if (fragmentController != null) {
// 4. 注册到全局路由(支持外部深链跳转到此 Controller)
Router.getInstance().holdController(
preferenceController, fragmentController.getHostFragment());
}
preferenceController.setDependKey(dependKey); // 5. 设置联动 key
return preferenceController;
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(
"Invalid preference controller: " + controllerName, e);
}
}📌 新手记住:写 Controller 子类时,构造器签名必须是 (Context, String, FragmentController, CarUxRestrictions),少一个参数、改一个类型,反射就 NoSuchMethodException 直接崩。看任何一个 Controller 子类,构造器都是这四件套——这是反射强约束,不是建议。
4.3 性能优化点
注意 PreferenceControllerListHelper.java:81-86 有一段 AI 加的优化方法 getPreferenceControllersFromMetadata()。它的作用是:原来 onAttach、onCreatePreferences、隐藏特性检查会各自解析一遍 XML,现在合并成”解析一次、复用三次”。这也是为什么 BaseXmlParserSettingsFragment.onAttach() 里能直接传 prefMetadataList 进来复用。
五、Fragment 如何持有和管理 Controller
5.1 BaseXmlParserSettingsFragment 的核心字段
文件:base/settingsBaseUi/src/main/java/com/android/car/settings/common/BaseXmlParserSettingsFragment.java
// 文件: base/settingsBaseUi/.../common/BaseXmlParserSettingsFragment.java:127-131
private final Map<Class, List<PreferenceController>> mPreferenceControllersLookup =
new ArrayMap<>(); // 按 Class 查找用(use() 方法)
protected final List<PreferenceController> mPreferenceControllers =
new ArrayList<>(); // 全部 Controller 列表
protected final List<String> mHiddenFeatures =
new ArrayList<>(); // 需要隐藏的功能 key
protected final Map<String, ArrayList<String>> mDependKeyMap =
new HashMap<>(); // 联动关系:开关 → 它控制的子项 keys5.2 onAttach:解析 + 实例化 + 注册 Lifecycle
// 文件: base/settingsBaseUi/.../common/BaseXmlParserSettingsFragment.java:301-322(精简)
int xmlResId = getPreferenceScreenResId();
List<Bundle> prefMetadataList = PreferenceXmlParser.extractMetadata(
styledContext, xmlResId, /* flags */);
mPreferenceControllers.addAll(
PreferenceControllerListHelper.getPreferenceControllersFromMetadata(
styledContext, prefMetadataList, /* fragmentController= */ this, mUxRestrictions));
mPreferenceControllers.forEach(controller -> {
lifecycle.addObserver(controller); // ← 关键:注册成 Lifecycle 观察者
mPreferenceControllersLookup
.computeIfAbsent(controller.getClass(), k -> new ArrayList<>(1))
.add(controller);
});5.3 onCreatePreferences:Controller ↔ Preference 绑定
// 文件: base/settingsBaseUi/.../common/BaseXmlParserSettingsFragment.java:588-608
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(getPreferenceScreenResId());
PreferenceScreen screen = getPreferenceScreen();
for (PreferenceController controller : mPreferenceControllers) {
// 按 key 在 screen 里找 Preference
Preference pref = screen.findPreference(controller.getPreferenceKey());
controller.setDependKeyList(mDependKeyMap.get(controller.getPreferenceKey()));
controller.setPreference(pref); // ← 在这里完成绑定 + 类型校验 + 注册监听
}
removeHiddenList(); // 删除隐藏项
removePreferencesIfNeed(); // 删除子类返回的待移除项
}controller.setPreference(pref) 这个方法(PreferenceController.java:329-338)做了三件事,非常重要:
final void setPreference(Preference preference) {
PreferenceUtil.requirePreferenceType(preference, getPreferenceType()); // 1. 类型校验
mPreference = getPreferenceType().cast(preference); // 2. 强转存字段
mPreference.setOnPreferenceChangeListener(
(changedPref, newValue) -> handlePreferenceChanged(...)); // 3a. 注册变更监听
mPreference.setOnPreferenceClickListener(
clickedPref -> handlePreferenceClicked(...)); // 3b. 注册点击监听
checkInitialized();
}从这里能看出:用户点击/改变开关,最终会自动调到你重写的 handlePreferenceChanged / handlePreferenceClicked。你不需要自己去 setOnClickListener。
📌 新手记住:一个 Controller 永远对应一个 Preference,靠 android:key 匹配。如果你的 Controller 写了但 XML 里没对应 key 的 Preference,setPreference(null) 会出问题。XML 里的 key 和 Controller 的 preferenceKey 必须一一对应。
5.4 use() 方法:在 Fragment 里反向拿 Controller
// 文件: base/settingsBaseUi/.../common/BaseXmlParserSettingsFragment.java:224-242
protected <T extends PreferenceController> T use(Class<T> clazz,
@StringRes int preferenceKeyResId) {
List<PreferenceController> controllerList = mPreferenceControllersLookup.get(clazz);
if (controllerList != null) {
String preferenceKey = getString(preferenceKeyResId);
for (PreferenceController controller : controllerList) {
if (controller.getPreferenceKey().equals(preferenceKey)) {
return (T) controller;
}
}
}
return null;
}这个方法用在 Fragment 需要给 Controller 传额外参数 的场景(比如 Fragment 拿到一个 ID,要塞给某个 Controller)。注释里给了标准用法:
@Override
public void onAttach(Context context) {
super.onAttach(context);
use(MyPreferenceController.class, R.string.pk_my_key).setMyArg(myArg);
}但项目规范是”少用 use()”——这是紧耦合的标志。能用 XML 配置、用信号驱动的,就别用 use()。
5.5 灯光页面的 Controller 装配图
LightsSettingsFragment 加载 miauto_lights_settings_fragment.xml,最终装配出下图结构:
flowchart TB subgraph Fragment["LightsSettingsFragment (java)"] direction TB F1["mPreferenceControllers: List"] end subgraph XML["miauto_lights_settings_fragment.xml"] direction TB X1["外灯 NewTabPreference<br/>key=pk_settings_externallights_entry"] X2["雾灯 MiCarLoadingButtonPreference<br/>key=pk_settings_fog_light_button_entry"] X3["大灯延时 NewTabPreference"] X4["自适应远光灯 MiCarNewSwitchPreference<br/>key=pk_settings_high_beam_auto_adjust_entry"] X5["...还有 20+ 个"] subgraph Cat1["Category: 车控灯"] X1 X2 X3 X4 end subgraph Cat2["Category: 氛围灯"] XA["氛围灯总开关"] XB["氛围灯模式"] XC["..."] end end subgraph Controllers["运行期实例化的 Controller 对象"] direction TB C1["ExteriorLightsTabLayPreferenceController<br/>extends BaseTabPreferenceController"] C2["FogLightButtonController<br/>extends ..."] C3["HeadLightsDelayTabLayoutPrefController"] C4["HighBeamAutoAdjustPrefController<br/>extends BaseVehicleNewSwitchPrefController<br/>← 继承链见第八节"] CA["AtmosphereLightTabLayoutPrefController"] CB["AtmosphereModeTabPrefController"] end X1 -.->|"settings:controller="| C1 X2 -.->|"settings:controller="| C2 X3 -.->|"settings:controller="| C3 X4 -.->|"settings:controller="| C4 XA -.-> CA XB -.-> CB F1 ==>|持有引用| C1 F1 ==>|持有引用| C2 F1 ==>|持有引用| C3 F1 ==>|持有引用| C4 F1 ==>|持有引用| CA
六、Activity 承载机制:FragmentHost
6.1 BaseCarSettingsActivity 的核心实现
文件:app/src/main/java/com/android/car/settings/common/BaseCarSettingsActivity.java
// 文件: app/src/main/java/com/android/car/settings/common/BaseCarSettingsActivity.java:81-84
public abstract class BaseCarSettingsActivity extends AppCompatActivity implements
FragmentHost, OnUxRestrictionsChangedListener, UxRestrictionsProvider,
SettingsFragment.IToolbarHolder, OnBackStackChangedListener,
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
IBaseCarSettingsActivity {它实现了 5 个接口,每个都有职责:
| 接口 | 职责 |
|---|---|
FragmentHost | 提供 launchFragment/goBack,让 Fragment 能跳页面 |
OnUxRestrictionsChangedListener | 监听驾驶状态变化(行车时禁用某些设置) |
UxRestrictionsProvider | 给 Fragment 查当前 CarUxRestrictions |
SettingsFragment.IToolbarHolder | 暴露 Toolbar 给 Fragment |
OnPreferenceStartFragmentCallback | 处理 Preference 上 android:fragment 跳转 |
6.2 launchFragment 的两种模式
// 文件: app/src/main/java/com/android/car/settings/common/BaseCarSettingsActivity.java:367-380
@Override
public void launchFragment(Fragment fragment) {
if (fragment instanceof DialogFragment) {
throw new IllegalArgumentException(
"cannot launch dialogs with launchFragment() - use showDialog() instead");
}
if (mIsSinglePane) { // 单屏模式:单独开一个 Activity
Intent intent = SubSettingsActivity.newInstance(/* context= */ this, fragment);
startActivity(intent);
} else { // 双屏模式:当前 Activity 内 replace Fragment
launchIfDifferent(fragment);
}
}mIsSinglePane 来自 AndroidManifest.xml 里 Activity 的 metadata 配置(见 BaseCarSettingsActivity.java:617)。这是为了适配单/双屏车型。
📌 新手记住:业务代码里跳页面永远只调 launchFragment(fragment) 或 getFragmentController().launchFragment(fragment),不要直接 getFragmentManager().beginTransaction()。框架已经帮你处理了单双屏差异、动画、回退栈。自己操作 FragmentManager 容易把回退栈搞乱。
6.3 SubSettingsActivity:单屏车型的二级页
// 文件: app/src/main/java/com/android/car/settings/common/SubSettingsActivity.java:30-62
public class SubSettingsActivity extends BaseCarSettingsActivity {
public static Intent newInstance(@NonNull Context context, @NonNull Fragment fragment) {
String fragmentClass = fragment.getClass().getName();
Intent intent = new Intent(context, SubSettingsActivity.class);
intent.putExtra(KEY_SUB_SETTINGS_FRAGMENT, fragmentClass);
intent.putExtra(KEY_SUB_SETTINGS_FRAGMENT_ARGS, fragment.getArguments());
return intent;
}
@Override
protected Fragment getInitialFragment() {
// 从 Intent 取 Fragment 类名,反射实例化
String fragmentClass = getIntent().getStringExtra(KEY_SUB_SETTINGS_FRAGMENT);
Fragment fragment = getSupportFragmentManager().getFragmentFactory()
.instantiate(getClassLoader(), fragmentClass);
fragment.setArguments(fragmentArgs);
return fragment;
}
}单屏时点二级菜单,会 startActivity(SubSettingsActivity.newInstance(...)),在新 Activity 里把目标 Fragment 反射创建出来作为初始 Fragment。
6.4 FragmentController:Controller 看到的”Fragment”
Controller 不直接持有 Fragment,而是通过 FragmentController 接口操作(文件:base/settingsBaseUi/.../common/FragmentController.java)。这个接口的核心方法:
public interface FragmentController {
void launchFragment(Fragment fragment); // 跳页面
Fragment getHostFragment(); // 拿到自己(实现类就是 Fragment 本身)
Activity getActivity();
void goBack(); // 返回
void showDialog(DialogFragment df, String tag);
void showDialog(int dialogId);
Preference findPreferenceByKey(String key); // 跨 Controller 查 Preference
PreferenceController findControllerByKey(String key); // 跨 Controller 查 Controller
void onHandleCmd(Bundle cmd); // Fragment ↔ Controller 的命令通道
Lifecycle getSettingsLifecycle();
<T extends ViewModel> T getViewModel(Class<T> modelClass);
// ...
}BaseXmlParserSettingsFragment 实现了这个接口(BaseXmlParserSettingsFragment.java:98)。所以你在 Controller 里写 getFragmentController().launchFragment(...) 实际上是在调它宿主 Fragment 的方法。
七、XML 声明 → 实例化 → 数据回流 完整数据流
flowchart TB subgraph 编译期["编译期:XML 声明"] A1["res/xml/miauto_lights_settings_fragment.xml"] A2["写法:<br/><MiCarNewSwitchPreference<br/> android:key='@string/pk_high_beam'<br/> settings:controller='com.xxx.HighBeamAutoAdjustPrefController'/>"] A1 --- A2 end subgraph 启动期["启动期:实例化"] direction TB B1["Activity.onCreate<br/>launchFragment(LightsSettingsFragment)"] B2["Fragment.onAttach<br/>PreferenceXmlParser.extractMetadata(xml)<br/>→ 提取 Bundle 列表"] B3["PreferenceControllerListHelper<br/>.getPreferenceControllersFromMetadata<br/>→ 反射 new 出所有 Controller"] B4["lifecycle.addObserver(controller)<br/>Controller 进入生命周期"] B5["Fragment.onCreatePreferences<br/>findPreference(key) → setPreference(pref)<br/>绑定 Controller ↔ Preference"] B1 --> B2 --> B3 --> B4 --> B5 end subgraph 运行期["运行期:数据双向流动"] direction TB C1["用户拨开关"] C2["Preference.setOnPreferenceChangeListener<br/>→ Controller.handlePreferenceChanged"] C3["Controller 下发车辆信号<br/>CarPropertyManager.setProperty"] C4["车辆回信号<br/>onHandlePropertyChange"] C5["preference.setChecked(...) 刷新 UI"] C1 --> C2 --> C3 C3 -.->|"异步"| C4 C4 --> C5 end subgraph 数据回流["数据回流:Fragment ↔ Controller 通信"] D1["Controller.requestDataFromHost(key)<br/>→ Fragment.onHandleCmd"] D2["Fragment 拉数据后<br/>回调 Controller.onResponse"] D1 --> D2 end 编译期 ==> 启动期 启动期 ==> 运行期 运行期 -.-> 数据回流
真实 XML 片段(带中文注释)
<!-- 文件: settingsPage/micarLightSettings/src/main/res/xml/miauto_lights_settings_fragment.xml:51-58 -->
<!-- 自适应远光灯开关 -->
<com.android.car.settings.miauto.preferences.MiCarNewSwitchPreference
android:key="@string/pk_settings_high_beam_auto_adjust_entry"
android:title="@string/settings_allow_activation_with_light_paddles_title"
android:summary="@string/settings_allow_activation_with_light_paddles_summary"
settings:hiddenFeatures="@string/micar_common_settings_lemans_ece"
settings:controller="com.android.car.settings.miauto.lights.HighBeamAutoAdjustPrefController" />几个属性的作用:
android:key:Controller和Preference配对的钥匙(必须,且唯一)settings:controller:Controller的全限定类名(框架反射用)settings:dependKey:联动开关,指定父开关的 key(父关 → 自己隐藏)settings:hiddenFeatures:声明在某些车型上隐藏(运行期检查mHiddenFeatures)settings:supportVoiceAssist:是否参与语音助手描述上报
八、真实 Controller 例子:HighBeamAutoAdjustPrefController
这是自适应远光灯开关的完整实现,55 行就搞定了一个完整业务。
文件:settingsPage/micarLightSettings/src/main/java/com/android/car/settings/miauto/lights/HighBeamAutoAdjustPrefController.java
// 继承链:HighBeamAutoAdjustPrefController
// → BaseVehicleNewSwitchPrefController(车辆开关基类,封装了信号订阅、loading、防抖)
// → CarPropertyMgrPreferenceController(车辆信号管理基类)
// → PreferenceController<MiCarNewSwitchPreference>(最底层抽象)
public class HighBeamAutoAdjustPrefController extends BaseVehicleNewSwitchPrefController {
// 1. 构造器:四件套签名,原样转交父类
public HighBeamAutoAdjustPrefController(Context context, String preferenceKey,
FragmentController fragmentController,
CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
}
// 2. onCreateInternal:页面创建时刷新 title/summary(车型差异)
@Override
protected void onCreateInternal() {
super.onCreateInternal();
if (CarConfigManager.INSTANCE.isAHBHighBeamConfig()) {
getPreference().setTitle(R.string.settings_allow_auto_with_light_paddles_title);
getPreference().setSummary(R.string.settings_allow_auto_with_light_paddles_summary);
}
}
// 3. 声明订阅哪个车辆信号
@Override
protected ArraySet<Integer> getPropertyIdSet() {
ArraySet<Integer> ids = new ArraySet<>();
ids.add(MiCarPropertyIds.LightCtrl.ADAPT_HIGH_BEAM_SWITCH);
return ids;
}
// 4. 信号值 → 开关状态:AUTO 视为开
@Override
protected boolean isPropertyOpen(int propVal) {
return propVal == Light.AdaptHighBeamSwitch.AUTO;
}
// 5. 开关状态 → 信号值:开则发 AUTO,关则发 OFF
@Override
protected int getPropertyVal(boolean isChecked) {
return isChecked ? Light.AdaptHighBeamSwitch.AUTO : Light.AdaptHighBeamSwitch.OFF;
}
}这就是这个项目里”加一个开关”的标准姿势:
- 继承
BaseVehicleNewSwitchPrefController(车控开关)或BaseSwitchPreferenceController(非车辆开关) - 实现 4-5 个抽象方法,告诉父类”订阅什么信号、信号值怎么映射到 UI”
- 在 XML 里加一行
<MiCarNewSwitchPreference settings:controller="..." />
整个文件里没有任何 Fragment、Activity、setOnClickListener、findViewById —— 业务代码变得极度纯粹。这就是范式的威力。
九、Controller 基类继承体系速览
classDiagram direction TB class PreferenceController~V~ { <<abstract>> +getAvailabilityStatus() int +onCreateInternal() +updateState(V) } class BaseSwitchPreferenceController { <<kotlin, 与信号无关的开关基类>> #onHandlePreCheck(Boolean) Boolean } class BaseTabPreferenceController { <<kotlin, 与信号无关的 Tab 基类>> +buildTabData() Tab~* } class CarPropertyMgrPreferenceController~V~ { <<abstract, 车辆信号管理基类>> #mCarPropertyManager +getProperty(int, int) CarPropertyValue +onHandlePropertyChange(CarPropertyValue, Boolean) } class BaseVehicleNewSwitchPrefController { <<kotlin, 车控开关基类>> #mConfirmDialog +isPropertyOpen(Int) Boolean +getPropertyVal(Boolean) Int } class BaseVehicleTabPreferenceController class BaseVehicleProgressPrefController class BaseVehicleButtonGroupPropController class HighBeamAutoAdjustPrefController { +getPropertyIdSet() +isPropertyOpen(Int) +getPropertyVal(Boolean) } class PositionA2LAMPSwitchController { +getPropertyIdSet() } PreferenceController <|-- BaseSwitchPreferenceController PreferenceController <|-- BaseTabPreferenceController PreferenceController <|-- CarPropertyMgrPreferenceController CarPropertyMgrPreferenceController <|-- BaseVehicleNewSwitchPrefController CarPropertyMgrPreferenceController <|-- BaseVehicleTabPreferenceController CarPropertyMgrPreferenceController <|-- BaseVehicleProgressPrefController CarPropertyMgrPreferenceController <|-- BaseVehicleButtonGroupPropController BaseVehicleNewSwitchPrefController <|-- HighBeamAutoAdjustPrefController BaseVehicleNewSwitchPrefController <|-- PositionA2LAMPSwitchController
业务 Controller 通常落在两条主继承链上:
- 车辆信号相关(车控开关、车控 Tab、车控滑块):
CarPropertyMgrPreferenceController→BaseVehicleNewSwitchPrefController/BaseVehicleTabPreferenceController/BaseVehicleProgressPrefController等 - 非车辆信号(普通 UI 开关、Tab、按钮):
PreferenceController→BaseSwitchPreferenceController/BaseTabPreferenceController
📌 新手记住:车辆相关的 Controller 基类在 base/settingsVehicleLib/,非车辆的在 base/settingsBaseUi/.../miauto/preferences/。下一篇会详讲车辆链路,本篇只要记住:你看到名字带 Vehicle 的 Controller,必然订阅了车辆信号,刷新逻辑在 onHandlePropertyChange 里,而不是 updateState 里。
十、为什么”加开关”的标准动作是新增 Controller + 改 XML?
这是本篇最重要的一节,把前面的所有内容串起来。
10.1 反例:把业务写进 Fragment
如果你把”自适应远光灯开关”的业务直接写在 LightsSettingsFragment 里,会怎样?
// 反面教材,不要这么写
class LightsSettingsFragment : SettingsFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
super.onCreatePreferences(savedInstanceState, rootKey)
val pref = findPreference<MiCarNewSwitchPreference>(R.string.pk_high_beam)
pref?.setOnPreferenceChangeListener { p, value ->
// 1. 下发信号
// 2. 订阅回信号
// 3. 弹对话框
// 4. 车型差异判断
// 5. License 校验
// ... 又是几十行
true
}
}
}灯光页 20 个开关全这么写,Fragment 直接 1000+ 行,所有人改同一文件冲突不断,没人能独立维护自己那块。
10.2 正例:拆 Controller
拆成 Controller 后:
- 每个
Controller独立一个文件,独立维护、独立测试、独立 code review - 改一个开关完全不动
Fragment,不会影响其他人 - 不同车型的差异(“哪些开关显示""title 怎么写”)通过
getAvailabilityStatus()和onCreateInternal()内聚到Controller自己 Controller是生命周期感知的,注册/反注册信号天然在onStart/onStop里成对出现,不会泄漏
10.3 标准动作清单
新增一个开关的完整步骤:
| 步骤 | 文件 | 动作 |
|---|---|---|
| 1 | res/values/strings.xml | 新增 pk_xxx(Preference key 字符串)和 UI 文案 |
| 2 | src/main/java/.../XxxPrefController.kt | 新建 Controller,继承 BaseSwitchPreferenceController 或 BaseVehicleNewSwitchPrefController |
| 3 | 同上 | 实现 getPreferenceType()、onCreateInternal()、(车辆类)getPropertyIdSet()/isPropertyOpen()/getPropertyVal() |
| 4 | res/xml/xxx_fragment.xml | 加一个 <MiCarNewSwitchPreference settings:controller="全限定类名" android:key="@string/pk_xxx" /> |
| 5 | (如需车型差异)XxxFragment.getPreferenceKeyResIdsToRemove() | 加上不支持的车型要隐藏的 key |
整个改动不碰 Fragment 的任何业务逻辑代码。
📌 新手记住:如果你发现改一个开关不得不动 Fragment 的 onCreatePreferences / onAttach 之外的代码,大概率是哪里设计有问题。要么是 Controller 基类能力不够,要么是这个开关根本不该是个开关(可能是另一个页面)。先停下来跟老同事对一下。
十一、关键文件索引
| 用途 | 文件路径 |
|---|---|
| Controller 抽象基类 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceController.java |
| XML → Controller 实例化 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceControllerListHelper.java |
| Fragment 装配 Controller 的地方 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/BaseXmlParserSettingsFragment.java |
| 最薄 Fragment 基类 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/BaseFragment.java |
| 业务 Fragment 基类 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/SettingsFragment.java |
| Activity 基类 | app/src/main/java/com/android/car/settings/common/BaseCarSettingsActivity.java |
| 二级页 Activity | app/src/main/java/com/android/car/settings/common/SubSettingsActivity.java |
| Controller 反向操作 Fragment 的接口 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/FragmentController.java |
| Fragment 请求 Activity 的接口 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/FragmentHost.java |
| 开关 Controller 业务基类(非车辆) | base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/preferences/BaseSwitchPreferenceController.kt |
| Tab Controller 业务基类(非车辆) | base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/preferences/BaseTabPreferenceController.kt |
| 车辆信号 Controller 基类 | base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/common/CarPropertyMgrPreferenceController.java |
| 车控开关 Controller 基类 | base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/BaseVehicleNewSwitchPrefController.kt |
| 真实页面示例:灯光 | settingsPage/micarLightSettings/src/main/java/com/android/car/settings/miauto/lights/LightsSettingsFragment.java |
| 真实 Controller 示例:自适应远光灯 | settingsPage/micarLightSettings/src/main/java/com/android/car/settings/miauto/lights/HighBeamAutoAdjustPrefController.java |
| 真实 XML 示例 | settingsPage/micarLightSettings/src/main/res/xml/miauto_lights_settings_fragment.xml |
十二、自检清单
读完本篇,你应该能回答以下问题:
-
PreferenceController的泛型<V>是什么作用?为什么必须有? -
getAvailabilityStatus()返回UNSUPPORTED_ON_DEVICE和返回CONDITIONALLY_UNAVAILABLE对 UI 有什么区别? - 一个
Controller写好了,但运行时Controller不工作(不打印日志),最可能的原因是什么?(提示:构造器签名 / XML key 不匹配 /getAvailabilityStatus返回 2) -
onStartInternal()里订阅了信号,对应的反订阅应该写在哪个方法里? - 为什么
Controller不直接持有Fragment,而是通过FragmentController接口? - 单屏车型和双屏车型,
launchFragment()的行为有什么不同? - 新增一个开关,最少要改几个文件?分别是什么?
如果某题答不上来,回到对应章节再看一遍源码。