02 - 核心架构:PreferenceController 范式

本篇概览

MiCarSettings 沿用了 AOSP Car Settings 的核心架构:PreferenceFragment + PreferenceController 组合模式。这个范式决定了你以后在这个项目里 90% 的业务怎么写、bug 怎么查、功能怎么加。

本篇聚焦三个问题:

  1. PreferenceController 是什么?它和 FragmentActivity 怎么协作?
  2. 一个写在 XML 里的 settings:controller="xxx" 字符串,是怎么变成一个跑在内存里、能收信号能刷 UI 的对象的?
  3. 为什么项目里”加一个开关”的标准动作是 新建一个 PreferenceController 子类 + 在 XML 里注册一行,而不是去改 Fragment

读完本篇你能掌握:

  • 看懂任何一门设置业务页面(灯光、充电、锁车……)的代码骨架
  • 知道一个 ControlleronCreateInternal()onDestroyInternal() 的完整生命周期,以及它和 Activity/Fragment 生命周期的对应关系
  • 理解 AvailabilityStatus 五种状态对 UI 的实际影响(什么时候会被隐藏、什么时候会被禁用)
  • 能独立完成”新增一个设置项”的全套改动:写 Controller、声明 XML、配置 key
  • 知道 BaseCarSettingsActivityBaseXmlParserSettingsFragmentPreferenceController 这条数据通路,定位问题能从 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 子类,不需要改 FragmentFragment 是公共骨架,改它会影响所有人。


二、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--:持有(组合关系)
  • ..>:依赖

关键关系链:

  1. Activity 实现 FragmentHostUxRestrictionsProvider 接口 —— 这是 Fragment 能挂载到 Activity 上的硬性约束(见 BaseFragment.java:119-124 的强校验)。
  2. BaseXmlParserSettingsFragment 实现了 FragmentController 接口 —— Controller 不直接持有 Fragment,而是通过 FragmentController 这个接口反向操作(launchFragmentgoBackshowDialogfindPreferenceByKey 等)。
  3. 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 类应该尽量”瘦”。所有”这个开关点击后做什么、这个标题显示什么、这个开关要不要隐藏”的判断,统统丢给 ControllerFragment 只决定”页面有哪些项、各项怎么排布”。


三、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 {

两个关键点:

  1. 泛型 <V extends Preference>:子类必须声明自己管的是哪种子 Preference(如 MiCarNewSwitchPreferenceNewTabPreferenceSliderPreference)。框架在 setPreference() 时会做类型校验,类型对不上直接抛 IllegalArgumentExceptionPreferenceController.java:329-338)。
  2. 实现 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()。它的作用是:原来 onAttachonCreatePreferences、隐藏特性检查会各自解析一遍 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<>();                                   // 联动关系:开关 → 它控制的子项 keys

5.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 的 PreferencesetPreference(null) 会出问题。XML 里的 keyControllerpreferenceKey 必须一一对应。

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监听驾驶状态变化(行车时禁用某些设置)
UxRestrictionsProviderFragment 查当前 CarUxRestrictions
SettingsFragment.IToolbarHolder暴露 ToolbarFragment
OnPreferenceStartFragmentCallback处理 Preferenceandroid: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.xmlActivitymetadata 配置(见 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/>&lt;MiCarNewSwitchPreference<br/>  android:key='@string/pk_high_beam'<br/>  settings:controller='com.xxx.HighBeamAutoAdjustPrefController'/&gt;"]
        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:keyControllerPreference 配对的钥匙(必须,且唯一)
  • settings:controllerController 的全限定类名(框架反射用)
  • 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;
    }
}

这就是这个项目里”加一个开关”的标准姿势:

  1. 继承 BaseVehicleNewSwitchPrefController(车控开关)或 BaseSwitchPreferenceController(非车辆开关)
  2. 实现 4-5 个抽象方法,告诉父类”订阅什么信号、信号值怎么映射到 UI”
  3. 在 XML 里加一行 <MiCarNewSwitchPreference settings:controller="..." />

整个文件里没有任何 FragmentActivitysetOnClickListenerfindViewById —— 业务代码变得极度纯粹。这就是范式的威力。


九、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、车控滑块):CarPropertyMgrPreferenceControllerBaseVehicleNewSwitchPrefController / BaseVehicleTabPreferenceController / BaseVehicleProgressPrefController
  • 非车辆信号(普通 UI 开关、Tab、按钮):PreferenceControllerBaseSwitchPreferenceController / BaseTabPreferenceController

📌 新手记住车辆相关的 Controller 基类在 base/settingsVehicleLib/,非车辆的在 base/settingsBaseUi/.../miauto/preferences/。下一篇会详讲车辆链路,本篇只要记住:你看到名字带 VehicleController,必然订阅了车辆信号,刷新逻辑在 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 标准动作清单

新增一个开关的完整步骤:

步骤文件动作
1res/values/strings.xml新增 pk_xxx(Preference key 字符串)和 UI 文案
2src/main/java/.../XxxPrefController.kt新建 Controller,继承 BaseSwitchPreferenceControllerBaseVehicleNewSwitchPrefController
3同上实现 getPreferenceType()onCreateInternal()、(车辆类)getPropertyIdSet()/isPropertyOpen()/getPropertyVal()
4res/xml/xxx_fragment.xml加一个 <MiCarNewSwitchPreference settings:controller="全限定类名" android:key="@string/pk_xxx" />
5(如需车型差异)XxxFragment.getPreferenceKeyResIdsToRemove()加上不支持的车型要隐藏的 key

整个改动不碰 Fragment 的任何业务逻辑代码

📌 新手记住如果你发现改一个开关不得不动 FragmentonCreatePreferences / 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
二级页 Activityapp/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() 的行为有什么不同?
  • 新增一个开关,最少要改几个文件?分别是什么?

如果某题答不上来,回到对应章节再看一遍源码。