Skip to content

17 · 常见陷阱与速查表

这是查字典用的一章,不按顺序读。遇到问题回来翻。


一、命令速查

日常

bash
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   # 运行练习场

代码生成

bash
fvm dart run build_runner build --delete-conflicting-outputs   # 生成
fvm dart run build_runner watch --delete-conflicting-outputs   # 监听
fvm dart run build_runner clean                                # 清理

校验与测试

bash
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         # 快速预检

出包

bash
scripts/build.sh android --release
scripts/build.sh ios --release
scripts/build.sh android --release --env prod

专项校验

bash
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 heightListViewColumnExpanded 包裹,或 SizedBox(height: AppTokens.spacingXxxxl)
An InputDecorator…cannot have an unbounded widthTextFieldRowExpanded 包裹
RenderFlex overflowed by X pixels子节点超出父约束Expanded + TextOverflow.ellipsis
Incorrect use of ParentData widgetExpanded 不在 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 valueAppTokens 是 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()异步后没检查 mountedif (!mounted) return;
LateInitializationErrorlate 变量没赋值就用检查初始化时机,或改用可空类型
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 速查(高频)

取上下文

dart
final l10n = RuntimeI18n.ofNonNull(context);   // 文案
final tokens = AppTokens.of(context);          // 主题(或 context.appTokens)

Riverpod

dart
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()                            // 重新请求

数据层

dart
return switch (result) {
  Ok(value: final data) => process(data),
  ErrorResult(error: final e) => throw e,
};

guard 选型:单对象 guardApiObject / 列表 guardApiList / 动作型 guardApiVoid / Map guardApiObjectMap

分页

dart
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);
}

导航

dart
context.push(Routes.x, extra: Args(...));   // 压栈
context.go(Routes.x);                        // 替换栈
context.pop(result);                         // 返回并带结果
final r = await context.push<bool>(Routes.x);

提示

dart
HeLoading.showToast('提示');
HeLoading.showSuccess('成功');
HeLoading.showError('失败');

日志

dart
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.spacerMdAppTokens.spacingMd
AppTokens.paddingS / paddingLAppTokens.spacingSm / spacingXl
AppTokens.radiusM / radiusSAppTokens.radiusLarge / radiusSmall
AppTokens.bodyMediumAppTokens.bodyMdRegular
AppTokens.fontTitleAppTokens.titleMdBold(按场景选)
AppTokens.listMaxHeight无此 token,用 spacingXxxxlExpanded

五、前端转 Flutter 的 15 个思维陷阱

按踩坑频率排序:

1. 在 build() 里做副作用

dart
Widget build(context) {
  repository.fetch();      // ❌ 每次重建都发请求
  return ...;
}

:副作用放 ViewModel 的 build()initState()

2. 在 build() 里 new 对象 / 算重活

dart
Widget build(context) {
  final list = hugeList.map(expensive).toList();   // ❌ 每帧算一次
}

3. 没有事件冒泡,嵌套点击只有最内层触发

dart
GestureDetector(onTap: outer, child: GestureDetector(onTap: inner, ...))
// 点 inner:只有 inner 触发

:手动都调,或用 Listener。详见 05 章。

4. 忘记 .notifier

dart
ref.read(provider).method()          // ❌
ref.read(provider.notifier).method() // ✅

5. watch / read 用错地方

dart
onPressed: () => ref.watch(p)   // ❌ 回调里用 watch
Widget build(...) => ref.read(p) // ❌ build 里该用 watch(除非只读一次)

6. 异步后没检查 mounted

dart
final d = await fetch();
setState(() => _d = d);        // ❌ 可能已 dispose

if (!mounted) return;          // ✅
setState(() => _d = d);

7. late 当可空用

dart
late String name;    // ❌ 如果它可能没有,用 String?
String? name;        // ✅

8. 忘了释放 controller

AnimationController / TextEditingController / StreamSubscription / Timer 都要在 dispose() 释放。

9. 用 constAppTokens getter

dart
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. 用字面量而非规范入口

dart
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.spacingXxl

14. 以为 build() 被调就是重绘了

build() 调用 ≠ 重新布局/绘制。中间有 Element diff。详见 03 章。

15. 用了过时的 API / 照抄旧文档

项目 docs/ 下有 4 篇 i18n 文档、.agents/skills/ 下有多个 skill,部分内容已过时。见下节。


六、⚠️ 过时内容清单(重要)

照抄以下内容会出错——它们存在于项目里,但已与代码不符。

文档类

来源过时内容正确做法
docs/README.md包名 package:aiwork/...package:haierenergy/...
docs/README.mdAppLocalizations.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-usagespacerMd / radiusM / bodyMediumspacingMd / radiusLarge / bodyMdRegular
.agents/skills/theme-usagebrand6Normal / fontGy1_85 / gray3 等 L1 色用 L3 语义色 tokens.textOnSurfaceHeading
.agents/skills/flutter-fix-layout-issuesAppTokens.paddingS / fontTitle / listMaxHeightspacingSm / titleMdBold / 无此 token
.agents/skills/common-component-usageimport 写 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.shbuild.shrun_prod.shcheck_project.dartcheck_figma_tokens.dartcheck_apifox_contracts.dartcheck_ui_dto_imports.dartfetch_iconfont.shfind_figma_component.dartscan_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

八、本文档系列索引

序号类别标题
0001-入门导览学习路线图
0102-Dart基础Dart 语言基础
0203-Flutter基础Flutter 核心基础
0303-Flutter基础Flutter 渲染原理与生命周期
0403-Flutter基础Flutter 布局与滚动进阶
0503-Flutter基础Flutter 交互与动画
0602-Dart基础Dart 异步并发与进阶语法
0704-项目架构项目架构总览
0804-项目架构状态管理 Riverpod
0904-项目架构数据层链路
1005-项目实战页面开发实战
1104-项目架构组件与主题
1205-项目实战分页与列表
1305-项目实战路由与导航
1406-平台能力国际化
1506-平台能力权限与设备能力
1607-工程化工程化与调试
1707-工程化常见陷阱与速查表 ← 你在这里

返回 → 专题首页