卷 VII · 手上CH 28深度 28/28

VS Code:把编辑器变成你的第二双手

直觉过境 「IDE 我熟,Android Studio 那套快捷键搬过来就行。」 改一下

Flutter 的缩进金字塔和两个类的样板,从来没打算让你手打。这一章不讲「推荐十个插件」,只做一件事:把那些真正省时间的入口过一遍,每一条都能对上具体的按键和具体的配置项。最后有一台真的 Quick Fix 台,让你亲眼看见 ⌘. 到底改了哪些字。

⌘. 二十一条reload vs restart尾逗号不用摆了settings.json

装什么:一个扩展,就一个

FlutterDart-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: 2insertSpaces: truedetectIndentation: falsedefaultFormatter,以及 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 / downchildren 里挪位置,不会弄坏括号和逗号
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 runrR
耗时通常几百毫秒一两秒
状态保住(第 9 章讲过为什么:Element 树没动)全丢,从 main() 重新来
会重跑吗只重跑 buildmain()initState() 都不会重跑全部重跑

官方文档明确列出的热重载看不到效果、必须热重启的情况:

  • 改了 main()initState() 里的代码(它们不会被重跑);
  • 改了全局变量或静态字段的初始值
  • 把枚举改成普通类、或者反过来;
  • 改了泛型类型声明
  • 改了原生代码(Kotlin / Java / Swift / Objective-C)——这个连热重启都不够,要完整重跑;
  • 代码有编译错误(这时候什么都不会生效,先看 Problems 面板);
  • 改了 CupertinoTabViewbuilder

还有一条不在官方列表里、但你一定会撞上的:改了 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 切换自动换行。

✎ 多设备与多环境:一份 launch.json
{
  "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 的精确对照

⇄ Compose 对照 · 你的工具肌肉能带走多少

能直接带走:Android Studio 的 ⌥⏎ → VS Code 的 ⌘.,都是「在这个位置我能干什么」的统一入口。F2 改名、⌘B 跳定义、Extract 系列,概念完全一致。

要重新长的:Compose 那边你依赖 Layout InspectorCompose Preview;Flutter 这边对应的是 Widget Inspector(DevTools)和 @Preview(3.35 才有,是个新东西)。另外 Compose 的重组高亮工具在 Flutter 这边叫 Highlight Repaints,找的问题是同一类。

没有对应物的:Compose 编译器会告诉你哪些函数是可跳过的(重组报告);Flutter 这边没有这种编译期报告——重建范围要你自己用 Wrap with ValueListenableBuilderconst 手动划。这正好是本书后半本一直在讲的那件手艺,也是这个 ⌘. 菜单里那几条 Builder 存在的理由。

这一章的一句话

Flutter 的样板不该手打:stless / stful 起骨架,⌘. 里那二十一条改结构,格式化交给 Dart 3.7 之后的 tall style,UI 问题交给 Inspector 和 Debug Painting——手打的部分越少,你花在「想清楚」上的时间越多。

折完了

二十八章到这里结束。回头看,这本书其实只讲了两件事:那棵你看不见的树(卷 II 到卷 V,为什么),和你手上真的要敲的东西(卷 VI 到卷 VII,怎么做)。

如果只留一句带走:你每帧写的只是一张平摊的纸,框架照着上一帧的折痕把它折成会量会画的东西。你在这本书里学到的每一个具体技巧——constKeysetState 的范围、BuildContext、两个类的分工——全都是在管那道折痕。

现在去把学校那个 App 做出来。第一行敲 stless