08 · 开发实战:新增一个设置页
本篇是培训文档集的实战篇。读完了 00(总览)、02(PreferenceController 范式)、03(路由)、05(车辆接口)之后,这一篇把它们串起来:手把手带你从零新增「一个二级设置页 + 一个车辆开关」,并支持语音打开、支持外部 URI 跳转,最后通过 detekt、推到 Gerrit。
一、本篇概览
需求(模拟)
在「灯光」一级页里,新增一个二级入口「智能灯光节能」(Smart Light Eco),点击进入新的设置页;新页面里有一个开关,开/关时读写车辆属性
LightCtrl.LIGHT_ECO_MODE,并在属性回流时更新 UI;同时:
- 支持外部 URI 跳转:其它 App / 桌面快捷方式用
carsettings://homepage/?subPage=vehicle_light_eco直接打开这个页。- 支持语音:用户说「打开智能灯光节能」由小爱路由进来。
读完你能掌握什么
- 在
settingsPage/下新增模块与复用现有模块的取舍与各自操作步骤 - 写一个
Fragment+ 一份页面 XML + 一个Controller,并让它们被框架正确装配 - 用
@RouterProvider+@Module注册路由,用@VoiceSearchProvider接入语音 - 多语言字符串的标准落位(
values-zh-rCN/values-en-rGB) - 本地
./gradlew checkStyleCode自查,按规范写 commit 并推 Gerrit - 至少 10 条「新手最容易踩」的坑
前置知识
- 已经能把工程编出来(见 01 篇)
- 知道 PreferenceController 范式:Fragment 加载 XML,XML 里用
settings:controller="全限定类名"声明 Controller,由PreferenceControllerListHelper反射实例化(见 02 篇) - 知道
@RouterProvider编译期被routerApt扫描、生成路由表(见 03 篇)
二、完整工作流(先看全景)
flowchart TD A["1. 选模块<br/>新建 or 复用 settingsPage/*"] --> B["2. 新增 Fragment<br/>继承 TopLevelSettingsFragment/SettingsFragment"] B --> C["3. 新增页面 XML<br/>src/main/res/xml/*.xml<br/>用 settings:controller 挂 Controller"] C --> D["4. 新增 Controller<br/>继承 BaseSwitchPreferenceController<br/>或 BaseVehicleNewSwitchPrefController"] D --> E["5. 注册路由<br/>@RouterProvider + @Module"] E --> F["6. 接入语音<br/>@VoiceSearchProvider"] F --> G["7. 多语言字符串<br/>values-zh-rCN / values-en-rGB"] G --> H["8. 一级页加入口<br/>在父页面 XML 加一个 NewTabPreference"] H --> I["9. 质量自查<br/>./gradlew checkStyleCode"] I --> J["10. 提交 & 推送<br/>git commit -s + push origin HEAD:refs/for/dev"] J --> K{"Gerrit 评审"} K -->|detekt 打回| I K -->|通过| L["合入 ✅"]
核心心智:Fragment 是页面的”名”,XML 是页面的”形”,Controller 是页面的”魂”。三者靠 XML 里
settings:controller这个属性串起来——这一点理解了,整篇就顺了。
三、文件改动 Checklist(一张图)
mindmap root((新增设置页)) 新模块(若新建) settings.gradle include settingsPage/xxx/build.gradle settingsPage/xxx/AndroidManifest.xml Fragment XxxFragment.kt 页面 XML src/main/res/xml/xxx_fragment.xml Controller XxxSwitchController.kt 路由 Fragment 加 @RouterProvider Fragment 加 @Module CarSettingsJump 加 URI 常量 PageAlias 加 pageAlias 语音 Fragment 加 @VoiceSearchProvider 字符串 values/strings.xml(key) values-zh-rCN/strings.xml values-en-rGB/strings.xml 一级页入口 父页面 XML 加 Preference 项 质量 checkStyleCode 通过 提交 commit -s push Gerrit
四、Step 1 · 选模块:新建还是复用?
settingsPage/ 下已经有 16 个模块(灯光、充电、锁车、驾驶……)。绝大多数新需求都应该加到现有模块里,不要新建——新建意味着新的 build.gradle、新的 kapt 配置、新的依赖图、新的编译时间,没有充分理由不划算。
什么时候新建模块?
- 这是一个独立的产品域,会有 5 个以上页面、自己的产品同学 owner
- 与现有任何模块的关注点都不同(例如新增”账号与隐私”域)
- 需要独立的 flavor 资源隔离策略
本例决定:复用 micarLightSettings
“智能灯光节能”属于灯光域,直接加到 settingsPage/micarLightSettings/ 即可。
如果真要新建模块,需要做两件事
1) 在 settings.gradle 注册(参考 settings.gradle:55 已有写法):
// settings.gradle —— 在其它 include 后面追加一行
include ":settingsPage:micarNewFeature"2) 新模块的 build.gradle 必须接入路由/语音 kapt(直接抄 settingsPage/micarLightSettings/build.gradle):
// settingsPage/micarNewFeature/build.gradle(精简版)
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt' // ★ 必须,路由/语音靠 kapt 编译期扫注解
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
flavorDimensions = ["miPlatform"]
productFlavors {
dcd { dimension "miPlatform" }
xcd { dimension "miPlatform" }
global { dimension "miPlatform" }
}
// ... sourceSets 三 flavor 的 srcDir 叠加规则见 07 篇
kotlinOptions { jvmTarget = '1.8' }
}
dependencies {
implementation project(":base:settingsBaseUi")
api project(":base:settingsBaseLib")
implementation project(":base:settingsVehicleLib")
// ★★★ 这两行是路由和语音的命脉,漏了就注解不生效
kapt project("${MiCarSettingsCommon_LibProjectName}:plugin:router:routerApt")
kapt project("${MiCarSettingsCommon_LibProjectName}:plugin:voice:voicesearchapt")
}📌 新手记住:本例不新建模块,直接在
micarLightSettings里加文件即可。下面所有 Step 都以复用现有模块为前提。
五、Step 2 · 新增 Fragment
参考真实例子
- Java 版:
settingsPage/micarLightSettings/src/main/java/com/android/car/settings/miauto/lights/LightsSettingsFragment.java - Kotlin 版(推荐新代码用 Kotlin):
settingsPage/micarChargeSettings/src/main/java/com/android/micar/settings/energy/EnergyManagerFragment.kt:31-50
关键观察(从真实代码里读出来的事实)
- Fragment 顶部叠了 4 个注解:
@VoiceSearchProvider、@Module(SettingsConstant.TAG_Light)、@RouterProvider(path = ...)、@MonitorFragment(埋点用,可省) - 继承
TopLevelSettingsFragment(或SettingsFragment),实现IPageRouteHandler - 必须实现两个方法:
getPageAlias()→ 返回页面别名,用于路由 & 语音getPreferenceScreenResId()→ 返回页面 XML 资源 id(编译期 R.xml.xxx)
- 可选覆写
getPreferenceKeyResIdsToRemove():返回一组R.string.pk_xxx,框架会按 key 把对应 Preference 从页面上动态隐藏(这是车型差异化的标准手段)
骨架代码(可照抄)
// AI(claude) auto generated at 2026-07-02
// 类名: LightEcoSettingsFragment
// 用途: 「智能灯光节能」二级设置页入口,加载 XML、声明路由与语音
// 作用: 路由/语音跳转的目标 Fragment,承担页面装配
package com.android.car.settings.miauto.lights
import android.net.Uri
import androidx.annotation.XmlRes
import com.android.car.settings.common.pageroute.CarSettingsJump
import com.android.car.settings.common.pageroute.PageAlias
import com.android.car.settings.common.SettingsConstant
import com.android.car.settings.miauto.TopLevelSettingsFragment
import com.android.car.settings.router.annotation.Module
import com.android.car.settings.router.annotation.RouterProvider
import com.android.car.settings.router.manager.IPageRouteHandler
import com.android.car.settings.voice.search.annotation.VoiceSearchProvider
import com.android.micar.settings.light.R
@VoiceSearchProvider // ① 接入语音搜索
@Module(SettingsConstant.TAG_Light) // ② 归属到「灯光」模块
@RouterProvider(path = CarSettingsJump.CarLightEco.LIGHT_ECO_URI_PATH) // ③ 注册 URI 路由
class LightEcoSettingsFragment : TopLevelSettingsFragment(), IPageRouteHandler {
override fun getPageAlias(): String = PageAlias.PAGE_LIGHT_ECO
@XmlRes
override fun getPreferenceScreenResId(): Int = R.xml.miauto_light_eco_settings_fragment
/** 车型差异化:不支持的配置项在这里隐藏 */
override fun getPreferenceKeyResIdsToRemove(): List<Int> = emptyList()
/** 处理外部 URI 携带的参数(如滚动定位、子项定位) */
override fun handleJump(data: Uri?) {
if (data != null) handleScrollUri(data)
}
}
// AI(claude) auto generated at 2026-07-02 END还要补两个常量(在对应的常量类里加)
(a) 在 PageAlias.kt 加 pageAlias(参考 base/settingsBaseUi/src/main/java/com/android/car/settings/common/pageroute/PageAlias.kt:15 已有的 PAGE_LIGHT):
// AI(claude) auto generated at 2026-07-02 BEGIN
const val PAGE_LIGHT_ECO = "vehicle_light_eco" // 智能灯光节能
// AI(claude) auto generated at 2026-07-02 END(b) 在 CarSettingsJump.kt 加 URI 常量(参考 CarSettingsJump.kt:72-74 的 LIGHT_URI_PATH):
// AI(claude) auto generated at 2026-07-02 BEGIN
object CarLightEco {
const val LIGHT_ECO_URI_PATH =
"$SETTING_SUBPAGE_URI_PREFIX$PAGE_LIGHT_ECO" // = "carsettings://homepage/?subPage=vehicle_light_eco"
}
// AI(claude) auto generated at 2026-07-02 END📌 新手记住:URI 协议头固定是
carsettings://homepage/?subPage=<别名>,由CarSettingsJump.SETTING_SUBPAGE_URI_PREFIX拼出来。别名(pageAlias)必须全局唯一,建议带模块前缀避免冲突。
六、Step 3 · 新增页面 XML
参考真实文件
settingsPage/micarLightSettings/src/main/res/xml/miauto_lights_settings_fragment.xml
真实的属性命名(已从源码确认,不要凭感觉写)
- 根节点命名空间:
xmlns:settings="http://schemas.android.com/apk/res-auto" - 挂 Controller 的属性是:
settings:controller="全限定类名"(不是controllerClass、不是app:controller) - 开关用的 Preference 类:
com.android.car.settings.miauto.preferences.MiCarNewSwitchPreference(项目自定义的,比 AOSP 原生SwitchPreference多了埋点/前置拦截能力) android:key用@string/pk_xxx而不是硬编码字符串(这是项目硬规范,便于getPreferenceKeyResIdsToRemove()动态隐藏)
本例页面 XML
新建文件:settingsPage/micarLightSettings/src/main/res/xml/miauto_light_eco_settings_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- AI(claude) auto generated at 2026-07-02 BEGIN -->
<!-- 文件: miauto_light_eco_settings_fragment.xml -->
<!-- 用途: 智能灯光节能设置页面布局,承载开关项 -->
<!-- 作用: 由 LightEcoSettingsFragment 加载,声明 Preference 与 Controller 的绑定关系 -->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="@string/psk_settings_light_eco_entry"
android:title="@string/preference_screen_title_light_eco">
<com.android.car.settings.miauto.preferences.SpacePreference
settings:spaceHeight="@dimen/micar_ui_middle_content_top_padding"/>
<com.android.car.settings.miauto.preferences.MiCarPreferenceCategory
android:key="@string/pk_settings_light_eco_category"
android:title="@string/micar_vehicle_control_light_eco">
<!-- 智能灯光节能总开关 -->
<com.android.car.settings.miauto.preferences.MiCarNewSwitchPreference
android:key="@string/pk_settings_light_eco_switch_entry"
android:title="@string/settings_light_eco_switch_title"
android:summary="@string/settings_light_eco_switch_summary"
settings:controller="com.android.car.settings.miauto.lights.LightEcoSwitchController" />
</com.android.car.settings.miauto.preferences.MiCarPreferenceCategory>
<com.android.car.settings.miauto.preferences.SpacePreference
settings:spaceHeight="@dimen/micar_ui_middle_content_bottom_padding"/>
</PreferenceScreen>
<!-- AI(claude) auto generated at 2026-07-02 END -->📌 新手记住:
android:key一律用@string/pk_xxx(pk = preference key),android:title用@string/...,严禁硬编码字符串——detekt 和 checkstyle 都会管,且海外版翻译靠它。
七、Step 4 · 新增 Controller(核心)
两种基类,二选一
| 场景 | 基类 | 路径 |
|---|---|---|
| 开关与车辆属性绑定(读写 CarProperty) | BaseVehicleNewSwitchPrefController | base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/BaseVehicleNewSwitchPrefController.kt |
| 开关与本地状态绑定(Settings.Global / SP / 自管) | BaseSwitchPreferenceController | base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/preferences/BaseSwitchPreferenceController.kt |
本例是车辆开关 → 继承 BaseVehicleNewSwitchPrefController。
真实范本(直接抄结构)
settingsPage/micarLightSettings/src/main/java/com/android/car/settings/miauto/lights/KunLunWelcomeLightSwitchController.kt 只有 30 行就完事——它把”读属性、写属性、判定开/关”抽象成三个 override,子类只关心业务映射。
本例 Controller 代码
// AI(claude) auto generated at 2026-07-02
// 类名: LightEcoSwitchController
// 用途: 「智能灯光节能」开关 Controller,读写车辆属性 LightCtrl.LIGHT_ECO_MODE
// 作用: 把开关 UI 的状态与车辆属性双向同步
package com.android.car.settings.miauto.lights
import android.car.drivingstate.CarUxRestrictions
import android.content.Context
import android.util.ArraySet
import com.android.car.settings.common.FragmentController
import com.android.car.settings.miauto.vehicle.BaseVehicleNewSwitchPrefController
import com.android.car.settings.miauto.vehicle.MiCarPropertyIds
import mi.car.config.Light
class LightEcoSwitchController(
context: Context?,
preferenceKey: String?,
fragmentController: FragmentController?,
uxRestrictions: CarUxRestrictions?
) : BaseVehicleNewSwitchPrefController(
context, preferenceKey, fragmentController, uxRestrictions
) {
/** 声明本 Controller 关心哪些车辆属性;属性变化时框架会回调 onHandlePropertyChange */
override fun getPropertyIdSet(): ArraySet<Int> = ArraySet<Int>().apply {
add(MiCarPropertyIds.LightCtrl.LIGHT_ECO_MODE)
}
/** 把"属性值"翻译成"开关是否打勾"。这里约定 ON=1 */
override fun isPropertyOpen(propVal: Int): Boolean {
return propVal == Light.OnOff.ON
}
/** 把"用户想要的开关状态"翻译成"要下发的属性值" */
override fun getPropertyVal(isChecked: Boolean): Int {
return if (isChecked) Light.OnOff.ON else Light.OnOff.OFF
}
}
// AI(claude) auto generated at 2026-07-02 ENDController 的工作机制(你要记清楚的几件事)
基类 BaseVehicleNewSwitchPrefController 已经替你做完了:
-
构造函数签名固定:必须是
(Context, String, FragmentController, CarUxRestrictions)——PreferenceControllerListHelper.createInstance()反射调用的是这个签名,写错参数个数 / 类型就实例化失败,开关直接不显示(这是新手最高频踩的坑,见第十节)。 真实代码可对照:base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceControllerListHelper.java:108-113。 -
onCreateInternal():页面创建时基类已经读一次属性并setChecked(...),所以子类通常不用自己写 onCreate。 -
handlePreferenceChanged(preference, newValue):开关被打勾时回调,基类已经把Boolean转成属性值并setVehicleProperty(...)下发,子类一般不用覆写。 -
onHandlePropertyChange(...):车辆属性变化(无论是不是本 App 改的)时框架回调,基类会自动preference.setChecked(...)——这就是”UI 回流”的入口。 -
getAvailabilityStatus():决定设置项的可见性 / 可用性,默认是AVAILABLE。常量定义在PreferenceController.java:79-104:
| 常量 | 值 | 效果 |
|---|---|---|
AVAILABLE | 0 | 正常显示、可点 |
CONDITIONALLY_UNAVAILABLE | 1 | 暂时不可用(条件不满足),后续可能恢复 |
UNSUPPORTED_ON_DEVICE | 2 | 彻底隐藏(不占位) |
DISABLED_FOR_PROFILE | 3 | 显示但禁用 |
AVAILABLE_FOR_VIEWING | 4 | 只读 |
要做车型差异化时,在子类 override 它:
override fun getAvailabilityStatus(): Int {
// AI(claude) auto generated at 2026-07-02 BEGIN
// 不支持灯光节能的车型,直接让设置项消失(不占位)
return if (CarConfigManager.isLightEcoSupported()) AVAILABLE else UNSUPPORTED_ON_DEVICE
// AI(claude) auto generated at 2026-07-02 END
}真实范本:settingsPage/micarDrivingSettings/src/main/java/com/android/micar/settings/driving/TowModeWarningPreferenceController.kt:43-46。
📌 新手记住:Controller 的构造函数签名和
settings:controller必须写全限定类名,这两件事是新手 80% 翻车的地方。先把这两件事做对,再谈业务。
八、Step 5 · 注册路由 @RouterProvider(已在 Fragment 完成)
回顾一下第五节 Fragment 顶部这两个注解:
@RouterProvider(path = CarSettingsJump.CarLightEco.LIGHT_ECO_URI_PATH)
@Module(SettingsConstant.TAG_Light)
class LightEcoSettingsFragment : ...它怎么生效的?
@RouterProvider/@Module定义在settingsCommon/plugin/router/annotation/...,RetentionPolicy.CLASS,编译期会被routerApt(settingsCommon/plugin/router/routerApt)扫到。routerApt为每个带注解的类生成一段”路由表条目”代码,编译产物里就有一张全局 URI → Fragment 的映射表。- 运行期外部 URI(如
carsettings://homepage/?subPage=vehicle_light_eco)进来,Router查表 → 找到LightEcoSettingsFragment→ 启动SubSettingsActivity→ 加载该 Fragment。 @Module的 value 是模块标签(SettingsConstant.kt里维护),用于路由按模块过滤、命中优先级。
⓵ 前提:模块的
build.gradle必须有kapt project("...:router:routerApt"),否则注解不被处理(见第四节)。micarLightSettings/build.gradle已配置好。
📌 新手记住:写完注解重新 build 一次(
./gradlew :settingsPage:micarLightSettings:assembleXcdDebug),让 kapt 跑完,路由才进表。改了 URI 路径要再 build。
九、Step 6 · 接入语音 @VoiceSearchProvider(已在 Fragment 完成)
@VoiceSearchProvider
class LightEcoSettingsFragment : ...它怎么生效的?
@VoiceSearchProvider定义在settingsCommon/plugin/voice/voicesearchannotation/.../VoiceSearchProvider.java,可带priority()(数值越大优先级越高)。voicesearchapt(settingsCommon/plugin/voice/voicesearchapt)编译期扫到这个注解,会把该 Fragment 对应页面 XML 的所有<Preference>(除了在getPreferenceKeyResIdsToRemove()里被隐藏的)作为”可被语音命中的设置项”写进VoiceSearchWidgetProvider。- 小爱侧拉起设置时,就靠这张表把”语音说法”映射到具体 Preference。
控制语音可见性
XML 里在某个 Preference 上加 settings:supportVoiceAssist="false",可单独让某个项不出现在语音问答里(真实例子:miauto_lights_settings_fragment.xml 里氛围灯相关项大量使用了该属性)。
📌 新手记住:
@VoiceSearchProvider加在 Fragment 上,不是 Controller 上;settings:supportVoiceAssist加在 XML 里的 Preference 上,不是 Fragment 上。两者别混。
十、Step 7 · 多语言字符串
落位规则
| 文件 | 用途 |
|---|---|
values/strings.xml | key 必须存在(fallback,通常写英文,也可省略内容只占位) |
values-zh-rCN/strings.xml | 国内中文(主语言,国内 dcd/xcd 走它) |
values-en-rGB/strings.xml | 海外英文(global flavor 走它) |
其它 values-xx/ | 海外多语言翻译,由翻译平台回填(见 07 篇) |
参考真实文件:
- 中文:
settingsPage/micarLightSettings/src/main/res/values-zh-rCN/strings.xml:8 - 英文:
settingsPage/micarLightSettings/src/main/res/values-en-rGB/strings.xml:6
本例需要新增的 string(每个文件都加,key 必须一致)
values-zh-rCN/strings.xml:
<!-- AI(claude) auto generated at 2026-07-02 BEGIN -->
<string name="preference_screen_title_light_eco">"智能灯光节能"</string>
<string name="psk_settings_light_eco_entry">"light_eco_screen"</string>
<string name="pk_settings_light_eco_category">"light_eco_category"</string>
<string name="pk_settings_light_eco_switch_entry">"light_eco_switch"</string>
<string name="micar_vehicle_control_light_eco">"灯光节能"</string>
<string name="settings_light_eco_switch_title">"智能灯光节能"</string>
<string name="settings_light_eco_switch_summary">"在夜间自动降低不必要的灯光功耗"</string>
<!-- AI(claude) auto generated at 2026-07-02 END -->values-en-rGB/strings.xml:
<!-- AI(claude) auto generated at 2026-07-02 BEGIN -->
<string name="preference_screen_title_light_eco">"Smart light eco"</string>
<string name="psk_settings_light_eco_entry">"light_eco_screen"</string>
<string name="pk_settings_light_eco_category">"light_eco_category"</string>
<string name="pk_settings_light_eco_switch_entry">"light_eco_switch"</string>
<string name="micar_vehicle_control_light_eco">"Light eco"</string>
<string name="settings_light_eco_switch_title">"Smart light eco"</string>
<string name="settings_light_eco_switch_summary">"Reduce unnecessary light power at night"</string>
<!-- AI(claude) auto generated at 2026-07-02 END -->⚠️
pk_*/psk_*是 Preference Key 的字符串值(不是给用户看的文案),但仍然要走 string 资源,方便代码用R.string.pk_xxx引用、方便getPreferenceKeyResIdsToRemove()隐藏。这些不要翻译,三种语言里写一样的值即可。
📌 新手记住:每个新 key 至少要在
values/、values-zh-rCN/、values-en-rGB/三处都加,少一处就有 lint 报错或翻译平台告警。文案翻译不要自己写其它语种——交给翻译平台回填。
十一、Step 8 · 在父页面加入口
要让用户能点进来,需要在「灯光」一级页 XML 里加一个入口项。打开 settingsPage/micarLightSettings/src/main/res/xml/miauto_lights_settings_fragment.xml,在合适位置追加:
<!-- AI(claude) auto generated at 2026-07-02 BEGIN -->
<!-- 智能灯光节能入口,点击路由到 LightEcoSettingsFragment -->
<com.android.car.settings.miauto.preferences.NewTabPreference
android:key="@string/pk_settings_light_eco_entry"
android:title="@string/preference_screen_title_light_eco"
settings:controller="com.android.car.settings.miauto.lights.LightEcoEntryPreferenceController" />
<!-- AI(claude) auto generated at 2026-07-02 END -->入口 Controller 一般继承 PreferenceController,点击时由路由框架跳转(具体跳转方式由 NewTabPreference / 父 Fragment 处理;这里给出最简骨架):
// AI(claude) auto generated at 2026-07-02 BEGIN
class LightEcoEntryPreferenceController @JvmOverloads constructor(
context: Context,
preferenceKey: String,
fragmentController: FragmentController? = null,
uxRestrictions: CarUxRestrictions? = null
) : PreferenceController<Preference>(context, preferenceKey, fragmentController, uxRestrictions) {
override fun getPreferenceType(): Class<Preference> = Preference::class.java
override fun handlePreferenceClicked(preference: Preference): Boolean {
// 通过路由框架跳到 LightEcoSettingsFragment
fragmentController?.launchFragment(LightEcoSettingsFragment())
return true
}
}
// AI(claude) auto generated at 2026-07-02 END(具体跳转 API 视项目里的 FragmentController 封装而定;如果项目用 URI 跳转,改成 context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(CarSettingsJump.CarLightEco.LIGHT_ECO_URI_PATH))) 也可。)
📌 新手记住:只有父页面入口项的
android:key会被LightsSettingsFragment.getPreferenceKeyResIdsToRemove()控制显隐。新入口想跟着车型配置走,记得在父 Fragment 的 remove 列表里加判断。
十二、Step 9 · 质量自查
# 全量(慢但全)
./gradlew checkStyleCode
# 只检查本次改动的文件(推荐,秒级)—— 见 checkstyle.gradle:88-92
./gradlew checkStyleCode -PchangedKotlinFiles="$(git diff --name-only HEAD | tr '\n' ' ')"
./gradlew javaCheckstyle -PchangedJavaFiles="$(git diff --name-only HEAD | tr '\n' ' ')"checkStyleCode 这个 task 在 checkstyle.gradle:96-100 定义,本质是:
task('checkStyleCode') {
dependsOn("javaCheckstyle") // Java:用 settingsCommon/tools/checkstyle/checkstyle.xml
dependsOn("detekt") // Kotlin:用 settingsCommon/tools/checkstyle/detekt.yml
}常见 detekt 规则打回原因
- 类/方法命名不符合规范(驼峰、Controller 后缀等)
- 文件末尾没空行 / import 顺序乱(detekt:formatting 插件管)
when必须有else、禁止用!!(项目 detekt.yml 比默认更严)- 过长方法 / 过多参数(
LongMethod、ParameterListWrapping) - 魔法数字没命名成常量
Gerrit 上的自动检查(PREUPLOAD.cfg)
项目根目录 PREUPLOAD.cfg 配置了:
ktlint_hook:对所有改动的.kt文件跑 ktlintcheckstyle_hook:对 commit 跑 checkstylecommit_msg_changeid_field:commit message 必须有Change-Id:
所以本地过了 detekt 还不够,push 后 Gerrit 还会再跑一遍 ktlint,不过照样打回。
📌 新手记住:push 前在本地跑一次
./gradlew checkStyleCode -PchangedKotlinFiles=...,比 Gerrit 上挂红牌快十倍。
十三、开关 → 写车辆属性 → UI 回流(时序图)
sequenceDiagram autonumber actor U as 用户 participant XML as MiCarNewSwitchPreference<br/>(XML 里的开关) participant Ctrl as LightEcoSwitchController participant Base as BaseVehicleNewSwitchPrefController participant Car as CarPropertyManager<br/>(android.car.*) participant Veh as 车控服务 Note over U,Veh: 阶段一:进入页面,读属性初始化开关 U->>XML: 进入「智能灯光节能」页 XML->>Base: PreferenceControllerListHelper 反射创建 Ctrl<br/>(传 Context,key,FragmentCtrl,UxRestrict) Base->>Ctrl: onCreateInternal() Ctrl->>Base: getPropertyIdSet() → {LIGHT_ECO_MODE} Base->>Car: getIntProperty(LIGHT_ECO_MODE, areaId) Car->>Veh: 读取车辆属性 Veh-->>Car: 当前值 = ON Car-->>Base: 1 Base->>Ctrl: isPropertyOpen(1) → true Ctrl-->>Base: true Base->>XML: preference.setChecked(true) Note over U,Veh: 阶段二:用户点击开关,写属性 U->>XML: 点击开关 → false XML->>Base: handlePreferenceChanged(pref, false) Base->>Ctrl: getPropertyVal(false) → OFF(0) Ctrl-->>Base: 0 Base->>Car: setVehicleProperty(LIGHT_ECO_MODE, 0, areaId) Car->>Veh: 下发到车控 Base-->>XML: return true(开关保持 false) Note over U,Veh: 阶段三:属性回流,UI 同步 Veh-->>Car: CarPropertyValue 回调 (异步) Car->>Base: onHandlePropertyChange(value=0, updateUI=true) Base->>Ctrl: isPropertyOpen(0) → false Base->>XML: preference.setChecked(false)(确认 UI 一致) Note right of Base: 即使用户没点、属性被外部改了,<br/>UI 也会自动同步
这张图要记住三件事:
- 初始化走
onCreateInternal → getIntProperty → isPropertyOpen → setChecked - 写走
handlePreferenceChanged → getPropertyVal → setVehicleProperty - 回流走
onHandlePropertyChange → isPropertyOpen → setChecked(异步、外部改动也触发)
十四、Step 10 · 提交与推送
14.1 检查改动
git status
git diff HEAD14.2 写 commit message(按 CLAUDE.md / AGENTS.md 规范)
git add settingsPage/micarLightSettings/src/main/java/com/android/car/settings/miauto/lights/LightEcoSettingsFragment.kt \
settingsPage/micarLightSettings/src/main/java/com/android/car/settings/miauto/lights/LightEcoSwitchController.kt \
settingsPage/micarLightSettings/src/main/res/xml/miauto_light_eco_settings_fragment.xml \
settingsPage/micarLightSettings/src/main/res/values/strings.xml \
settingsPage/micarLightSettings/src/main/res/values-zh-rCN/strings.xml \
settingsPage/micarLightSettings/src/main/res/values-en-rGB/strings.xml \
base/settingsBaseUi/src/main/java/com/android/car/settings/common/pageroute/PageAlias.kt \
base/settingsBaseUi/src/main/java/com/android/car/settings/common/pageroute/CarSettingsJump.kt
git commit -s -m "[Feature][All-Vehicle][micar] 新增智能灯光节能设置页
新增 LightEcoSettingsFragment 二级页面,包含一个开关,
读写车辆属性 LightCtrl.LIGHT_ECO_MODE,并支持 carsettings:// URI 跳转
与小爱语音打开。
AI: Claude 2026-07-02
Co-authored-by: Claude <noreply@anthropic.com>
Jira: MICOCKPIT-XXXXX"字段对照(来自 CLAUDE.md 提交规范):
[Feature]:新功能(修 bug 用[Bugfix],重构用[Refactor],详见 CLAUDE.md)[All-Vehicle]:未指定车型时默认;指定车型写[Lemans-RC09]之类[micar]:模块名,可选AI: Claude 日期:紧跟详细描述,与 Jira 之间空一行Jira:/Meego::bugfix 用 Jira,需求用 Meego,二选一-s:自动注入Signed-off-by;Change-Id由 Gerrit hook 自动加,不要手写
14.3 推到 Gerrit
# 远程名先用 git remote -v 确认(本项目实际是 origin)
git remote -v
# origin ssh://...slave.auto.mioffice.cn:29418/.../MiCarSettings
git push origin HEAD:refs/for/dev📌 新手记住:分支名(这里
dev)一定要跟团队当前主干对齐,推错分支要回滚很麻烦。Gerrit 评审页:https://gerrit.auto.mioffice.cn
十五、踩坑清单(10 条,都是真实血泪)
1. 忘了在 settings.gradle 注册新模块
症状:Gradle sync 报 Project ':settingsPage:micarNewFeature' not found.
解法:settings.gradle 加 include ":settingsPage:micarNewFeature",与现有 16 个模块写法一致(settings.gradle:46-61)。
2. 模块 build.gradle 漏了 kapt 路由/语音
症状:@RouterProvider 写了,但外部 URI 跳不进来;@VoiceSearchProvider 写了,语音也命不中。
解法:build.gradle 必须有:
kapt project("${MiCarSettingsCommon_LibProjectName}:plugin:router:routerApt")
kapt project("${MiCarSettingsCommon_LibProjectName}:plugin:voice:voicesearchapt")改完要 clean rebuild,否则旧的生成代码还在。
3. Controller 没在 XML 里 settings:controller="..." 声明
症状:Fragment 能进,但开关”看不见”或”点了没反应”——其实 Controller 根本没被实例化。
解法:PreferenceControllerListHelper.java:90-92 会跳过 controller 字段为空的 Preference。检查 XML 里这一行确实写了全限定类名。
4. Controller 构造函数签名写错
症状:页面进来直接 crash,日志 NoSuchMethodException。
解法:PreferenceControllerListHelper.createInstance()(PreferenceControllerListHelper.java:108-113)反射调用的是 (Context, String, FragmentController, CarUxRestrictions) 四参构造,类型 / 顺序 / 可空性都不能错。Kotlin 里参数加 ? 与不加是两个不同的签名。
5. getAvailabilityStatus() 返回错值,开关”消失”
症状:开关凭空不见,但 XML 明明写了。
解法:UNSUPPORTED_ON_DEVICE 会彻底隐藏(不占位);想”显示但灰显”用 DISABLED_FOR_PROFILE 或 AVAILABLE_FOR_VIEWING;想”暂时隐藏以后再出现”用 CONDITIONALLY_UNAVAILABLE。常量定义在 PreferenceController.java:79-104。
6. android:key 硬编码字符串
症状:detekt/checkstyle 报 Do not use hard-coded strings,或 getPreferenceKeyResIdsToRemove() 拿不到 key。
解法:所有 android:key 一律 @string/pk_xxx,并在 values*/strings.xml 同步新增。参考 miauto_lights_settings_fragment.xml 全篇都是这种写法。
7. 字符串只加了中文,没加 values/ 和 values-en-rGB/
症状:Lint 报 MissingTranslation;海外版本构建时 key 直接 missing。
解法:每个新 key 在 values/、values-zh-rCN/、values-en-rGB/ 三处都加,海外其它语种交给翻译平台回填。
8. flavor 资源放错目录
症状:dcd 上能看到的功能,xcd/global 编译报 Cannot resolve symbol,或运行报 Resource not found。
解法:看模块 build.gradle 的 sourceSets(micarLightSettings/build.gradle:42-62):
- 公共代码/资源放
src/main/ - 仅 dcd 用的放
src/dcddif/ - 仅 xcd 用的放
src/xcddif/ - 仅 global 用的放
src/global/AIDL 文件也要按 flavor 放 aidl srcDir——globalflavor 的 aidl 路径是src/main/aidl + src/xcddif/aidl + src/global/aidl,跟 java/res 都不同,特别坑。
9. 改了 @RouterProvider 的 path 没重新 build
症状:URI 改了路径,外部跳转还是命中老路径。
解法:路由表是 kapt 编译期生成的,改完注解必须 ./gradlew :settingsPage:micarLightSettings:assembleXcdDebug 触发 APT,否则运行期查到的还是旧表。如果 IDE 增量编译抽风,跑一次 ./gradlew clean 再编。
10. Change-Id 重复或丢失
症状:Gerrit commit_msg_changeid_field hook 报错;或同一改动推上去变成两个 change。
解法:
- 首次提交用
git commit -s,hook 自动注入Change-Id,不要手写 - 追加改动用
git commit --amend,保留原Change-Id,不要-s重新生成 - cherry-pick 到别的分支时手动改
Change-Id的I开头那串,避免冲突
附:少见的坑(备查)
- 11. 平台签名缺失 → 调用
CarPropertyManager静默失败:本 App 必须是 platform 签名的系统特权应用,调试包用platform.keystore签(项目根目录已有)。换 debug 默认签名会导致 Car API 大量不可用。 - 12. AIDL 接口跨 flavor 不一致 →
globalflavor 少了某个 aidl 文件,编译报cannot find symbol:检查sourceSets.global.aidl.srcDirs是否包含了对应目录。
十六、新增设置页 Checklist(可勾选)
把这一节复制到你的 MR 描述里,逐条勾选,评审会快很多。
□ 1. 模块决策:新建模块已在 settings.gradle 注册;复用模块已确认归属
□ 2. Fragment:继承 TopLevelSettingsFragment,实现 IPageRouteHandler
□ 3. Fragment 注解:@RouterProvider / @Module / @VoiceSearchProvider 三个齐
□ 4. pageAlias 与 URI 常量:PageAlias.kt + CarSettingsJump.kt 各加一条
□ 5. 页面 XML:src/main/res/xml/xxx_fragment.xml,根节点 PreferenceScreen
□ 6. Preference 的 settings:controller 写了全限定类名
□ 7. Preference 的 android:key 用 @string/pk_xxx,未硬编码
□ 8. Controller:构造函数签名 = (Context, String, FragmentController, CarUxRestrictions)
□ 9. Controller:getPreferenceType() 返回正确类型(MiCarNewSwitchPreference)
□ 10. Controller:getPropertyIdSet() / isPropertyOpen() / getPropertyVal() 全实现
□ 11. Controller:车型差异通过 getAvailabilityStatus() 返回 UNSUPPORTED_ON_DEVICE 隐藏
□ 12. 父页面入口:在父页面 XML 加 Preference 项
□ 13. 多语言:每个 key 在 values/、values-zh-rCN/、values-en-rGB/ 三处都加
□ 14. AI 注释规范:所有 AI 生成代码都有 BEGIN/END 标记(// AI(claude) auto generated at YYYY-MM-DD)
□ 15. 本地 ./gradlew checkStyleCode 通过(增量用 -PchangedKotlinFiles=...)
□ 16. commit message:[类型][车型][模块] 简述 + 详细 + AI: Claude 日期 + Jira/Meego
□ 17. git commit -s 自动注入 Signed-off-by 与 Change-Id
□ 18. git push origin HEAD:refs/for/<分支>,Gerrit ktlint/detekt 通过
十七、参考文件索引(按出现顺序)
| 文件 | 用途 |
|---|---|
settings.gradle | 模块注册 |
settingsPage/micarLightSettings/build.gradle | 模块 build 模板(kapt 路由/语音) |
settingsPage/micarLightSettings/.../LightsSettingsFragment.java | Java Fragment 范本 |
settingsPage/micarChargeSettings/.../EnergyManagerFragment.kt:31-50 | Kotlin Fragment 范本 |
settingsPage/micarLightSettings/src/main/res/xml/miauto_lights_settings_fragment.xml | XML 范本(settings:controller 真实写法) |
settingsPage/micarLightSettings/.../KunLunWelcomeLightSwitchController.kt | 车辆开关 Controller 范本(Kotlin) |
settingsPage/micarLightSettings/.../HighBeamAutoAdjustPrefController.java | 车辆开关 Controller 范本(Java) |
settingsPage/micarDisplaySettings/.../HudSnowModePrefController.kt | 非车辆开关 Controller 范本 |
base/settingsVehicleLib/.../BaseVehicleNewSwitchPrefController.kt | 车辆开关基类 |
base/settingsBaseUi/.../BaseSwitchPreferenceController.kt | 普通开关基类 |
base/settingsBaseUi/.../PreferenceController.java:79-104 | AvailabilityStatus 常量 |
base/settingsBaseUi/.../PreferenceControllerListHelper.java:90-113 | XML → Controller 反射实例化 |
settingsCommon/plugin/router/annotation/.../RouterProvider.java | @RouterProvider 定义 |
settingsCommon/plugin/router/annotation/.../Module.java | @Module 定义 |
settingsCommon/plugin/voice/voicesearchannotation/.../VoiceSearchProvider.java | @VoiceSearchProvider 定义 |
base/settingsBaseUi/.../pageroute/SettingsConstant.kt | 模块标签 |
base/settingsBaseUi/.../pageroute/PageAlias.kt:15 | pageAlias 命名规范 |
base/settingsBaseUi/.../pageroute/CarSettingsJump.kt:16-17,72-74 | URI 协议头与拼装 |
settingsPage/micarDrivingSettings/.../TowModeWarningPreferenceController.kt:43-46 | getAvailabilityStatus() 真实用法 |
checkstyle.gradle:96-100 | checkStyleCode task 定义 |
PREUPLOAD.cfg | Gerrit 提交钩子(ktlint + changeid) |
最后一句话:新增设置页这件事,第一次做会觉得”步骤好多”,做完一次就发现全是套路。把这个 Checklist 跑通一遍,之后所有新需求都是复制粘贴 + 改业务。祝顺利合入 🚗。