VS Code:把编辑器变成你的第二双手
Flutter 的缩进金字塔和两个类的样板,从来没打算让你手打。这一章不讲「推荐十个插件」,只做一件事:把那些真正省时间的入口过一遍,每一条都能对上具体的按键和具体的配置项。最后有一台真的 Quick Fix 台,让你亲眼看见 ⌘. 到底改了哪些字。
装什么:一个扩展,就一个
Flutter(Dart-Code.flutter)。它会自动带上 Dart-Code.dart-code,这两个就是全部。装完之后你已经拥有了:语法高亮、补全、跳转、重构、调试、热重载、DevTools、Widget 预览、格式化。
常被推荐但其实不必装的几类:
- 代码片段扩展(Awesome Flutter Snippets 之类)。
stless/stful/stanim这三个最常用的片段来自 Dart 分析服务器本身,装了插件就有。 - Error Lens 之类的错误行内显示。Dart 的分析器本来就把错误标在行内、列在 Problems 面板里;再叠一层容易和 closing labels 打架。
- 格式化插件。Dart 的格式化器在 SDK 里,扩展已经把
editor.defaultFormatter指向自己了。装 Prettier 那类东西只会互相抢。
另外,扩展装好后会自动给 [dart] 配好这几项,你不用再写:editor.tabSize: 2、insertSpaces: true、detectIndentation: false、defaultFormatter,以及 editor.inlayHints.enabled: "offUnlessPressed"(内联类型提示平时藏着,按住 ⌥ 才浮出来——这个默认值很聪明,别改)。
核心只有一个键:⌘.
Android Studio 那边是 ⌥⏎,VS Code 这边是 ⌘.(Windows / Linux 是 Ctrl+.)。Flutter 开发九成的重构都从这一个键进去。光标放在一个 widget 上按它,弹出来的是这些——下面这份清单是从本机 Flutter 3.44.8 的分析服务器里直接读出来的,不是凭印象写的:
| 类别 | 条目 | 用来干什么 |
|---|---|---|
| 往外包一层 | Wrap with Center / Container / SizedBox | 最常用的三个容器 |
Wrap with Padding | 自动填一个 const EdgeInsets.all(8.0) 等你改 | |
Wrap with Column / Row / Expanded / Flexible | 把一个 widget 变成一组,或者让它参与 flex 分配 | |
Wrap with Builder / ValueListenableBuilder / StreamBuilder / FutureBuilder | 缩小重建范围的主力(第 14 章),以及接异步数据 | |
| 动结构 | Remove this widget | 拆掉一层。拆缩进金字塔时天天用 |
Move widget up / down | 在 children 里挪位置,不会弄坏括号和逗号 | |
Swap with parent / child | 把这一层和上/下一层对调(比如让 Padding 跑到 Center 外面) | |
| 换类 | Convert to StatefulWidget | 长出第二个类,并把所有 name 自动改写成 widget.name |
Convert to StatelessWidget | 反向降级 | |
| 抽出去 | Extract Widget | 这条最值钱:把一坨抽成新的 StatelessWidget 类,会先问你类名 |
Extract Method | 抽成一个返回 Widget 的方法(比抽类轻,但拿不到 const 的好处) | |
Extract Local Variable | 抽成局部变量 |
亲手按一下:它到底改了哪些字
光看清单没有感觉。下面这台机器里,左边那些大写开头的 widget 名字都能点——点一个就是把光标放上去,右边立刻列出这个位置真的会弹出来的选项,再点一条就当场改写。
这台改写器不是拿字符串拼的近似品:它的每一条输出都和本机 dart language-server 通过 LSP 给出的编辑逐字符核对过(连「Wrap with Builder 生成的代码在 } 后面漏了一个逗号」这种真实的小毛病也照抄了)。
玩过之后有两件事值得记住。第一,Wrap with Center 放得下就放一行,Wrap with Padding 不管放不放得下都拆行——这是模板写死的,不是格式化器决定的。第二,光标放在类名上时,菜单里只剩一条 Convert to StatefulWidget,但它是全书出现频率最高的一条:这就是第 25 章说「默认写无状态、需要时再升级」几乎零成本的原因。
热重载与热重启:分界线在哪
这是每天要按几百次的两个动作,分清楚能省掉大量「改了怎么没反应」的困惑。
| 热重载 Hot Reload | 热重启 Hot Restart | |
|---|---|---|
| 按什么 | ⌘S 保存,或 Ctrl+F5 | ⇧⌘F5(就是 VS Code 的「重启调试」) |
终端里跑 flutter run 时 | r | R |
| 耗时 | 通常几百毫秒 | 一两秒 |
| 状态 | 保住(第 9 章讲过为什么:Element 树没动) | 全丢,从 main() 重新来 |
| 会重跑吗 | 只重跑 build。main() 和 initState() 都不会重跑 | 全部重跑 |
官方文档明确列出的热重载看不到效果、必须热重启的情况:
- 改了
main()或initState()里的代码(它们不会被重跑); - 改了全局变量或静态字段的初始值;
- 把枚举改成普通类、或者反过来;
- 改了泛型类型声明;
- 改了原生代码(Kotlin / Java / Swift / Objective-C)——这个连热重启都不够,要完整重跑;
- 代码有编译错误(这时候什么都不会生效,先看 Problems 面板);
- 改了
CupertinoTabView的builder。
还有一条不在官方列表里、但你一定会撞上的:改了 pubspec.yaml 或换了依赖,要停掉重跑。
dart.flutterHotReloadOnSave 默认是 "manual"——只有你手动 ⌘S 才热重载,编辑器自动保存不算。可选值还有 never / manualIfDirty / all / allIfDirty。
如果你开了 files.autoSave,想让自动保存也触发重载就设成 "all";但更常见的选择是反过来——关掉自动保存,让 ⌘S 成为「我改完了,去刷新」这个明确的动作。手指的节奏会清楚很多。
另外还有个新选项 dart.previewHotReloadOnSaveWatcher(默认关):改成用文件系统监听而不是 VS Code 的保存事件,这样在别的编辑器里改文件也会触发重载——用 AI 工具在外面改代码时很有用。
格式化:Dart 3.7 之后,尾逗号不用你摆了
这条更新对 Flutter 的日常影响比任何插件都大,但很多人还在用老习惯。
以前:格式化器的行为由你手摆的尾逗号控制——参数列表末尾有逗号就一个参数一行,没有就尽量挤在一行。于是「加逗号 / 删逗号」变成了一种手动排版。
Dart 3.7 起:格式化器换成了新的 tall style,它自己按行宽决定加还是删尾逗号。放得下就收成一行并删掉尾逗号,放不下就拆开并补上。你不需要再管这件事。
两个配置项都在 analysis_options.yaml 里(不再是 VS Code 设置——dart.lineLength 已经被官方标成 LEGACY):
# analysis_options.yaml
include: package:flutter_lints/flutter.yaml
formatter:
page_width: 100 # 默认 80。Flutter 代码嵌套深,100 舒服很多
trailing_commas: preserve # 3.8 起可用:想要回手动控制就写这个
linter:
rules:
- prefer_const_constructors # 帮你把 const 加满(第 4 章那个性能开关)
- prefer_const_literals_to_create_immutables
- use_key_in_widget_constructors # 提醒你别忘了收 key
- avoid_print # 用 debugPrint 或日志库
- use_build_context_synchronously # await 之后用 context 会被拦住
配好之后打开 editor.formatOnSave,从此不再想排版的事。全项目一次性修掉能自动修的问题:dart fix --apply(或者命令面板里的 Apply Fix All in Workspace)——它也是把 withOpacity 批量换成 withValues 那种迁移的正规做法。
看得见的辅助:三个开关
① Closing labels(默认开着)
多行调用的收尾括号后面那些灰色的 // CourseTile,就是它。深嵌套时它是唯一能让你知道「这个 ) 关的是谁」的东西。设置项是 dart.closingLabels。
② Flutter UI Guides(默认关着,建议打开)
在 widget 树的缩进处画出竖直的连线,一眼看出层级关系:
"dart.previewFlutterUiGuides": true "dart.previewFlutterUiGuidesCustomTracking": true # 减少更新延迟
官方文档提醒:这条线的连贯程度受编辑器行高影响,如果显示成虚线断断续续的,调一下 editor.lineHeight。另外它和 closing labels 同时开会有点挤,可以二选一。
③ Widget Preview(3.35 起内置,2026 年已经好用了)
在任意 widget 上加一个 @Preview 注解,侧边栏就能实时渲染它——不用启动模拟器、不用跑到那个页面去:
import 'package:flutter/widget_previews.dart'; @Preview(name: '课程卡片') Widget courseTilePreview() => const CourseTile(name: 'INFOSYS 722', room: '401');
命令面板里搜 Flutter: Show Widget Preview。相关设置:dart.flutterWidgetPreview(默认 startLazily,用到才启动)、dart.flutterWidgetPreviewLocation(默认 sidebar)。做设计稿还原和调组件样式时,这个循环比「改代码 → 热重载 → 手动点到那个页面」快一个数量级。
调试:不要用 print 找 UI 问题
| 你想知道 | 按什么 / 开什么 |
|---|---|
| 这块界面是哪段代码画的 | Flutter: Inspect Widget,然后在 App 里点一下 → 直接跳到源码那一行 |
| 为什么这里空了一块 / 对不齐 | Flutter: Toggle Debug Painting,所有边界、padding、baseline 全画出来 |
| 为什么掉帧 | ⌃⌥D 开 DevTools → Performance;重绘热点用 Highlight Repaints |
| 内存涨了不降 | DevTools → Memory(找忘了 dispose 的控制器) |
| 网络请求发了什么 | DevTools → Network |
| 这一帧的三棵树长什么样 | DevTools → Flutter Inspector 的 Widget Tree / Details Tree |
DevTools 的入口不止一个:命令面板里有 Dart: Open DevTools Widget Inspector Page 这类直达命令,能一步开到具体某个页面;也可以设 dart.openDevTools(默认绑 ⌃⌥D)。
在 await 之后的行上打断点,单步(F10)走出去时可能「跳到别的地方」——因为那之后的代码是作为回调排进事件队列的(第 18 章那台事件循环讲的就是这个)。这不是调试器坏了。
调异步逻辑时,用多个断点加 F5 继续,比逐行单步靠谱得多。
一份可以直接抄的 settings.json
{
// —— 编辑体验 ——
"dart.previewFlutterUiGuides": true,
"dart.previewFlutterUiGuidesCustomTracking": true,
"dart.renameFilesWithClasses": "prompt", // 改类名时问你要不要一起改文件名
"editor.formatOnSave": true,
"editor.rulers": [100], // 和 formatter 的 page_width 对齐
// —— 热重载 ——
"dart.flutterHotReloadOnSave": "manual", // 默认值,手动 ⌘S 才重载
"dart.hotReloadProgress": "statusBar", // 别用通知,太吵
// —— 少点噪音 ——
"dart.showTodos": false, // 看烦了就关
"files.exclude": { "**/.dart_tool": true, "**/.flutter-plugins-dependencies": true }
}
剩下值得知道的按键,都是 VS Code 通用的,只是在 Flutter 里格外好用:⌘P 跳文件、⇧⌘O 跳当前文件的符号、⌘T 全项目找类、F2 改名(配上 renameFilesWithClasses 顺手改文件名)、⌥⇧F 格式化整个文档、⌘K ⌘0 全部折叠(看一个巨大的 build 时救命)、⌥Z 切换自动换行。
{
"configurations": [
{
"name": "课表 · 开发环境",
"request": "launch", "type": "dart",
"args": ["--dart-define=API_BASE=https://dev.example.com"]
},
{
"name": "课表 · 生产环境",
"request": "launch", "type": "dart",
"args": ["--dart-define=API_BASE=https://api.example.com"]
}
],
"compounds": [
{ "name": "两台一起跑", "configurations": ["课表 · 开发环境", "课表 · 生产环境"] }
]
}
--dart-define 传进去的值用 String.fromEnvironment('API_BASE') 读,而且它是编译期常量,所以能写在 const 上下文里。compounds 那段可以让你同时在 iOS 模拟器和 Android 模拟器上跑同一份代码,改一行两边一起热重载——查平台差异时非常省事。
和 Compose 的精确对照
能直接带走:Android Studio 的 ⌥⏎ → VS Code 的 ⌘.,都是「在这个位置我能干什么」的统一入口。F2 改名、⌘B 跳定义、Extract 系列,概念完全一致。
要重新长的:Compose 那边你依赖 Layout Inspector 和 Compose Preview;Flutter 这边对应的是 Widget Inspector(DevTools)和 @Preview(3.35 才有,是个新东西)。另外 Compose 的重组高亮工具在 Flutter 这边叫 Highlight Repaints,找的问题是同一类。
没有对应物的:Compose 编译器会告诉你哪些函数是可跳过的(重组报告);Flutter 这边没有这种编译期报告——重建范围要你自己用 Wrap with ValueListenableBuilder 和 const 手动划。这正好是本书后半本一直在讲的那件手艺,也是这个 ⌘. 菜单里那几条 Builder 存在的理由。
这一章的一句话
Flutter 的样板不该手打:stless / stful 起骨架,⌘. 里那二十一条改结构,格式化交给 Dart 3.7 之后的 tall style,UI 问题交给 Inspector 和 Debug Painting——手打的部分越少,你花在「想清楚」上的时间越多。
折完了
二十八章到这里结束。回头看,这本书其实只讲了两件事:那棵你看不见的树(卷 II 到卷 V,为什么),和你手上真的要敲的东西(卷 VI 到卷 VII,怎么做)。
如果只留一句带走:你每帧写的只是一张平摊的纸,框架照着上一帧的折痕把它折成会量会画的东西。你在这本书里学到的每一个具体技巧——const、Key、setState 的范围、BuildContext、两个类的分工——全都是在管那道折痕。
现在去把学校那个 App 做出来。第一行敲 stless。