17 · 常见陷阱与速查表
这是查字典用的一章,不按顺序读。遇到问题回来翻。
一、命令速查
日常
fvm flutter pub get # 拉依赖
fvm flutter run # 运行(默认环境)
fvm flutter run -t lib/main_test.dart # 运行(测试环境后端)
fvm flutter run -t lib/main_prod.dart # 运行(生产环境后端)
fvm flutter run -t lib/main_playground.dart # 运行练习场代码生成
fvm dart run build_runner build --delete-conflicting-outputs # 生成
fvm dart run build_runner watch --delete-conflicting-outputs # 监听
fvm dart run build_runner clean # 清理校验与测试
fvm flutter analyze lib # 静态分析(要求 0 error)
fvm flutter test # 全部测试
fvm flutter test test/xxx_test.dart # 单个测试
fvm flutter test --plain-name "用例名" # 按名字跑
bash scripts/precheck.sh # 构建前预检(完整)
bash scripts/precheck.sh --quick # 快速预检出包
scripts/build.sh android --release
scripts/build.sh ios --release
scripts/build.sh android --release --env prod专项校验
fvm dart run scripts/check_project.dart # 统一守护
fvm dart run scripts/check_figma_tokens.dart # Figma token 漂移
fvm dart run scripts/check_apifox_contracts.dart # Apifox 契约
fvm dart run scripts/check_ui_dto_imports.dart # UI 层 DTO 导入审计二、报错速查
布局类
| 控制台错误 | 主因 | 修复 |
|---|---|---|
Vertical viewport was given unbounded height | ListView 在 Column 内 | Expanded 包裹,或 SizedBox(height: AppTokens.spacingXxxxl) |
An InputDecorator…cannot have an unbounded width | TextField 在 Row 内 | Expanded 包裹 |
RenderFlex overflowed by X pixels | 子节点超出父约束 | Expanded + TextOverflow.ellipsis |
Incorrect use of ParentData widget | Expanded 不在 Row/Column 直系 | 移到直系位置(外层 Expanded,内层 Padding) |
RenderBox was not laid out | 上述错误的级联 | ⚠️ 忽略,往上找主错误 |
排查第一原则:看到 RenderBox was not laid out 不要直接修,从堆栈栈顶往下找第一条含 unbounded / overflowed / cannot have an / Incorrect use of ParentData 的主错误。
详见 02-Flutter核心基础 第五节。
编译类
| 错误 | 原因 | 修复 |
|---|---|---|
Target of URI doesn't exist: 'xxx.g.dart' | 没跑 build_runner | 跑生成 |
The method 'xxx' isn't defined | 生成文件过期 | 跑 build_runner |
Const variables must be initialized with a constant value | AppTokens 是 getter,不能进 const | 去掉 const |
Undefined name 'AppLocalizations' | 用了过时的 i18n 入口 | 用 RuntimeI18n.ofNonNull(context) |
No ProviderScope found | 测试/练习场没包 ProviderScope | 包上(见 16 章测试模板) |
The argument type 'X' can't be assigned to 'Y' | 用了 DTO 而非 UI Model | 在 ViewModel 里转换 |
@CancelToken() 注解不存在 | 注解名写错 | 用 @CancelRequest()(带括号) |
| Riverpod 3.x 语法报错 | 照抄了新版教程 | 本项目是 2.6.1,按 08 章写 |
运行时类
| 错误 | 原因 | 修复 |
|---|---|---|
setState() called after dispose() | 异步后没检查 mounted | 加 if (!mounted) return; |
LateInitializationError | late 变量没赋值就用 | 检查初始化时机,或改用可空类型 |
Bad state: Stream has already been listened to | 单订阅 Stream listen 两次 | 用 .asBroadcastStream() |
A AnimationController was still active... | 忘了 dispose() | 在 dispose() 里释放 |
NoSuchMethodError: ... on null | 空安全没处理好 | 用 ?. / ?? / valueOrNull |
界面显示 home.title 原文 | i18n key 没配上 | 检查 JSON,或 JSON 语法错误 |
| 401 循环 | 登录接口误走 AuthInterceptor | 检查拦截器配置 |
| 取消请求却弹错误 toast | 没走 guard 函数 | 用 guardApiObject 等(已处理 cancel) |
三、API 速查(高频)
取上下文
final l10n = RuntimeI18n.ofNonNull(context); // 文案
final tokens = AppTokens.of(context); // 主题(或 context.appTokens)Riverpod
ref.watch(provider) // 订阅(build 里)
ref.watch(p.select((s) => s.field)) // 精准订阅
ref.read(provider) // 读一次(回调里)
ref.read(provider.notifier).method() // 调方法(别忘 .notifier)
ref.listen(p, (prev, next) => ...) // 副作用
ref.onDispose(() => ...) // 清理
ref.invalidateSelf() // 重新请求数据层
return switch (result) {
Ok(value: final data) => process(data),
ErrorResult(error: final e) => throw e,
};guard 选型:单对象 guardApiObject / 列表 guardApiList / 动作型 guardApiVoid / Map guardApiObjectMap。
分页
class _XBody extends PagedListBody<X> {
AsyncValue<PagedState<X>> watchState(WidgetRef ref) => ref.watch(xViewModelProvider);
Future<void> refresh(WidgetRef ref) => ref.read(xViewModelProvider.notifier).refresh();
Future<void> loadMore(WidgetRef ref) => ref.read(xViewModelProvider.notifier).loadMore();
Widget itemBuilder(BuildContext context, X item, int index) => XCard(x: item);
}导航
context.push(Routes.x, extra: Args(...)); // 压栈
context.go(Routes.x); // 替换栈
context.pop(result); // 返回并带结果
final r = await context.push<bool>(Routes.x);提示
HeLoading.showToast('提示');
HeLoading.showSuccess('成功');
HeLoading.showError('失败');日志
AppLogger.I.d('调试'); // 禁止 print()四、Token 速查(精简版)
完整版见 11-组件与主题。
| 类别 | 命名 | 能否 const |
|---|---|---|
| 间距 | spacingXxs/Xs/Sm/Md/Lg/Xl/Xxl/Xxxl/Xxxxl | ❌ |
| 圆角 | radiusSmall/Default/Large/ExtraLarge/ExtraExtraLarge/ExtraExtraExtraLarge/Full | ❌ |
| 字体 | {title,body}{Xl,Lg,Md,Sm,Xs}{Regular,Medium,Bold} | ❌ |
| 图标 | iconS/iconM/iconL/iconXL | ✅ |
| 组件 | buttonHeight/loadingIndicatorSize/dialogMaxWidth 等 | ✅ |
| 颜色 | tokens.brandPrimaryDefault / tokens.textOnSurfaceHeading 等 | ❌(需先取实例) |
⚠️ 旧命名已废弃
| 旧(不要用) | 新 |
|---|---|
AppTokens.spacerMd | AppTokens.spacingMd |
AppTokens.paddingS / paddingL | AppTokens.spacingSm / spacingXl |
AppTokens.radiusM / radiusS | AppTokens.radiusLarge / radiusSmall |
AppTokens.bodyMedium | AppTokens.bodyMdRegular |
AppTokens.fontTitle | AppTokens.titleMdBold(按场景选) |
AppTokens.listMaxHeight | 无此 token,用 spacingXxxxl 或 Expanded |
五、前端转 Flutter 的 15 个思维陷阱
按踩坑频率排序:
1. 在 build() 里做副作用
Widget build(context) {
repository.fetch(); // ❌ 每次重建都发请求
return ...;
}改:副作用放 ViewModel 的 build() 或 initState()。
2. 在 build() 里 new 对象 / 算重活
Widget build(context) {
final list = hugeList.map(expensive).toList(); // ❌ 每帧算一次
}3. 没有事件冒泡,嵌套点击只有最内层触发
GestureDetector(onTap: outer, child: GestureDetector(onTap: inner, ...))
// 点 inner:只有 inner 触发改:手动都调,或用 Listener。详见 05 章。
4. 忘记 .notifier
ref.read(provider).method() // ❌
ref.read(provider.notifier).method() // ✅5. watch / read 用错地方
onPressed: () => ref.watch(p) // ❌ 回调里用 watch
Widget build(...) => ref.read(p) // ❌ build 里该用 watch(除非只读一次)6. 异步后没检查 mounted
final d = await fetch();
setState(() => _d = d); // ❌ 可能已 dispose
if (!mounted) return; // ✅
setState(() => _d = d);7. late 当可空用
late String name; // ❌ 如果它可能没有,用 String?
String? name; // ✅8. 忘了释放 controller
AnimationController / TextEditingController / StreamSubscription / Timer 都要在 dispose() 释放。
9. 用 const 包 AppTokens getter
const EdgeInsets.all(AppTokens.spacingMd) // ❌ 编译失败
EdgeInsets.all(AppTokens.spacingMd) // ✅10. 列表有状态却不加 Key
删除列表项后状态错乱 → 加 ValueKey(item.id)。详见 03 章。
11. UI 层直接用 DTO
DTO 是后端契约,必须经 ViewModel 转成 UI Model。
12. 手写 EasyRefresh 装配分页
用 PagedListBody + HeListScaffold。详见 12 章。
13. 用字面量而非规范入口
Text('中文') // ❌ → l10n.t('key')
Color(0xFF0073E5) // ❌ → tokens.brandPrimaryDefault
SizedBox(height: 16) // ❌ → AppTokens.spacingMd
Image.asset('assets/images/a.png') // ❌ → Assets.images.a.image()
Icon(Icons.search) // ❌ → AppIcon(name: 'icon_search')
print('x') // ❌ → AppLogger.I.d('x')
Navigator.push(...) // ❌ → context.push(Routes.x)
SizedBox(height: 32.h) // ❌ → AppTokens.spacingXxl14. 以为 build() 被调就是重绘了
build() 调用 ≠ 重新布局/绘制。中间有 Element diff。详见 03 章。
15. 用了过时的 API / 照抄旧文档
项目 docs/ 下有 4 篇 i18n 文档、.agents/skills/ 下有多个 skill,部分内容已过时。见下节。
六、⚠️ 过时内容清单(重要)
照抄以下内容会出错——它们存在于项目里,但已与代码不符。
文档类
| 来源 | 过时内容 | 正确做法 |
|---|---|---|
docs/README.md | 包名 package:aiwork/... | package:haierenergy/... |
docs/README.md | AppLocalizations.of(context) | RuntimeI18n.ofNonNull(context) |
docs/README.md | 学习路径指向 lib/ui/audit/completion/ | 该目录已不存在(现为 device/ paramSet/ plant_detail/) |
docs/language.md 等 4 篇 i18n | 内容重复且部分过时 | 读本系列 14-国际化 |
Skill / 脚本类
| 来源 | 过时内容 | 正确做法 |
|---|---|---|
.agents/skills/theme-usage | spacerMd / radiusM / bodyMedium | spacingMd / radiusLarge / bodyMdRegular |
.agents/skills/theme-usage | brand6Normal / fontGy1_85 / gray3 等 L1 色 | 用 L3 语义色 tokens.textOnSurfaceHeading 等 |
.agents/skills/flutter-fix-layout-issues | AppTokens.paddingS / fontTitle / listMaxHeight | spacingSm / titleMdBold / 无此 token |
.agents/skills/common-component-usage | import 写 package:aiwork/he_components/... | package:haierenergy/he_components/... |
多处提及的 scripts/check_tdesign_imports.dart | 该脚本已不存在 | 用 scripts/check_project.dart(已包含 TDesign 检查) |
多处提及的 scripts/check_bottom_sheet_scaffold.dart | 已不存在 | — |
多处提及的 scripts/check_asset_strings.dart | 已不存在 | — |
scripts/ 实际存在:precheck.sh、build.sh、run_prod.sh、check_project.dart、check_figma_tokens.dart、check_apifox_contracts.dart、check_ui_dto_imports.dart、fetch_iconfont.sh、find_figma_component.dart、scan_figma_components.dart。
废弃代码链路
| 废弃项 | 状态 | 正确做法 |
|---|---|---|
PermissionService | 仅 5 个文件引用(定义+DI),业务零使用 | hasPermission('app:b:xxx') |
AppFeature | 枚举只剩 home 一个值 | 同上 |
UserInfo.menus | 已废弃 | 同上 |
lib/ui/device/ 的自建分页 | 历史遗留 | 新页面用通用四层骨架 |
七、每日自查清单
提交代码前过一遍:
- [ ] 所有命令加了
fvm前缀 - [ ] 改了模型/provider/ApiService → 跑了 build_runner
- [ ] 生成文件(
.g.dart/.freezed.dart)已提交 - [ ]
fvm flutter analyze lib→ 0 error - [ ]
fvm flutter test→ 全通过 - [ ]
bash scripts/precheck.sh→ 通过 - [ ] 没有硬编码中文(用
l10n.t()) - [ ] 没有硬编码颜色/尺寸(用
AppTokens) - [ ] 没有
Image.asset字面串(用Assets.images) - [ ] 没有
print()(用AppLogger) - [ ] 没有
Navigator.push(用context.push) - [ ] 异步后检查了
mounted - [ ] controller 都
dispose了 - [ ] 分页用了
PagedListBody(没手写 EasyRefresh) - [ ] 权限用了
hasPermission('app:b:xxx')(没用PermissionService)
八、本文档系列索引
| 序号 | 类别 | 标题 |
|---|---|---|
| 00 | 01-入门导览 | 学习路线图 |
| 01 | 02-Dart基础 | Dart 语言基础 |
| 02 | 03-Flutter基础 | Flutter 核心基础 |
| 03 | 03-Flutter基础 | Flutter 渲染原理与生命周期 |
| 04 | 03-Flutter基础 | Flutter 布局与滚动进阶 |
| 05 | 03-Flutter基础 | Flutter 交互与动画 |
| 06 | 02-Dart基础 | Dart 异步并发与进阶语法 |
| 07 | 04-项目架构 | 项目架构总览 |
| 08 | 04-项目架构 | 状态管理 Riverpod |
| 09 | 04-项目架构 | 数据层链路 |
| 10 | 05-项目实战 | 页面开发实战 |
| 11 | 04-项目架构 | 组件与主题 |
| 12 | 05-项目实战 | 分页与列表 |
| 13 | 05-项目实战 | 路由与导航 |
| 14 | 06-平台能力 | 国际化 |
| 15 | 06-平台能力 | 权限与设备能力 |
| 16 | 07-工程化 | 工程化与调试 |
| 17 | 07-工程化 | 常见陷阱与速查表 ← 你在这里 |
返回 → 专题首页