flutter-dart-code-review

flutter-dart-code-review

热门

与库无关的 Flutter/Dart 代码审查清单,涵盖 Widget 最佳实践、状态管理模式(BLoC、Riverpod、Provider、GetX、MobX、Signals)、Dart 惯用法、性能、无障碍、安全性和整洁架构。

23万Star
3.5万Fork
更新于 2026/7/17
SKILL.md
readonly只读
name
flutter-dart-code-review
description

与库无关的 Flutter/Dart 代码审查清单,涵盖 Widget 最佳实践、状态管理模式(BLoC、Riverpod、Provider、GetX、MobX、Signals)、Dart 惯用法、性能、无障碍、安全性和整洁架构。

Flutter/Dart 代码审查最佳实践

全面的、与库无关的 Flutter/Dart 应用程序审查清单。无论使用哪种状态管理方案、路由库或依赖注入框架,这些原则都适用。


1. 项目整体健康度

  • [ ] 项目遵循一致的文件夹结构(按功能或按层级)
  • [ ] 关注点分离得当:UI、业务逻辑、数据层
  • [ ] Widget 中不包含业务逻辑;Widget 纯粹是表现层
  • [ ] pubspec.yaml 整洁——无未使用的依赖,版本适当锁定
  • [ ] analysis_options.yaml 包含严格的 lint 规则集,并启用严格的分析器设置
  • [ ] 生产代码中无 print() 语句——使用 dart:developerlog() 或日志包
  • [ ] 生成的文件(.g.dart.freezed.dart.gr.dart)是最新的或已加入 .gitignore
  • [ ] 平台特定代码通过抽象隔离

2. Dart 语言陷阱

  • [ ] 隐式 dynamic:缺少类型注解导致 dynamic——启用 strict-castsstrict-inferencestrict-raw-types
  • [ ] 空安全误用:过度使用 !(强制解包运算符)而非正确的空检查或 Dart 3 模式匹配(if (value case var v?)
  • [ ] 类型提升失败:使用 this.field 而局部变量提升即可生效
  • [ ] 捕获范围过宽catch (e) 不带 on 子句;始终指定异常类型
  • [ ] 捕获 ErrorError 子类型表示程序错误,不应被捕获
  • [ ] 未使用的 async:标记为 async 但从未 await 的函数——不必要的开销
  • [ ] late 过度使用:在可空或构造函数初始化更安全的地方使用 late;将错误推迟到运行时
  • [ ] 循环中的字符串拼接:使用 StringBuffer 而非 + 进行迭代字符串构建
  • [ ] const 上下文中的可变状态const 构造函数类中的字段不应可变
  • [ ] 忽略 Future 返回值:使用 await 或显式调用 unawaited() 以表明意图
  • [ ] varfinal 即可:局部变量优先使用 final,编译时常量使用 const
  • [ ] 相对导入:使用 package: 导入以保持一致性
  • [ ] 暴露可变集合:公共 API 应返回不可修改的视图,而非原始 List/Map
  • [ ] 缺少 Dart 3 模式匹配:优先使用 switch 表达式和 if-case,而非冗长的 is 检查和手动类型转换
  • [ ] 为多返回值创建一次性类:使用 Dart 3 记录 (String, int) 而非一次性 DTO
  • [ ] 生产代码中的 print():使用 dart:developerlog() 或项目的日志包;print() 没有日志级别且无法过滤

3. Widget 最佳实践

Widget 分解:

  • [ ] 单个 Widget 的 build() 方法不超过约 80-100 行
  • [ ] Widget 按封装性 AND 按变化方式(重建边界)拆分
  • [ ] 返回 Widget 的私有 _build*() 辅助方法应提取为独立的 Widget 类(支持元素复用、const 传播和框架优化)
  • [ ] 在不需要可变局部状态时,优先使用 StatelessWidget 而非 StatefulWidget
  • [ ] 可复用的提取 Widget 放在单独的文件中

Const 使用:

  • [ ] 尽可能使用 const 构造函数——防止不必要的重建
  • [ ] 对于不变的集合使用 const 字面量(const []const {}
  • [ ] 当所有字段都是 final 时,构造函数声明为 const

Key 使用:

  • [ ] 在列表/网格中使用 ValueKey 以在重排时保持状态
  • [ ] 谨慎使用 GlobalKey——仅在确实需要跨树访问状态时使用
  • [ ] 避免在 build() 中使用 UniqueKey——它会强制每帧重建
  • [ ] 当标识基于数据对象而非单个值时使用 ObjectKey

主题与设计系统:

  • [ ] 颜色来自 Theme.of(context).colorScheme——不硬编码 Colors.red 或十六进制值
  • [ ] 文本样式来自 Theme.of(context).textTheme——不使用内联 TextStyle 和原始字号
  • [ ] 验证深色模式兼容性——不假设浅色背景
  • [ ] 间距和尺寸使用一致的设计令牌或常量,而非魔法数字

Build 方法复杂度:

  • [ ] build() 中不进行网络调用、文件 I/O 或重型计算
  • [ ] build() 中不使用 Future.then()async 工作
  • [ ] build() 中不创建订阅(.listen()
  • [ ] setState() 限定在尽可能小的子树中

4. 状态管理(与库无关)

这些原则适用于所有 Flutter 状态管理方案(BLoC、Riverpod、Provider、GetX、MobX、Signals、ValueNotifier 等)。

架构:

  • [ ] 业务逻辑位于 Widget 层之外——在状态管理组件中(BLoC、Notifier、Controller、Store、ViewModel 等)
  • [ ] 状态管理器通过注入接收依赖,而非内部构造
  • [ ] 服务或仓库层抽象数据源——Widget 和状态管理器不应直接调用 API 或数据库
  • [ ] 状态管理器具有单一职责——没有处理无关关注点的“上帝”管理器
  • [ ] 跨组件依赖遵循方案的约定:
    • Riverpod 中:provider 通过 ref.watch 依赖其他 provider 是预期的——仅标记循环或过度纠缠的链
    • BLoC 中:bloc 不应直接依赖其他 bloc——优先使用共享仓库或表示层协调
    • 在其他方案中:遵循组件间通信的文档约定

不可变性与值相等(适用于不可变状态方案:BLoC、Riverpod、Redux):

  • [ ] 状态对象是不可变的——通过 copyWith() 或构造函数创建新实例,绝不就地修改
  • [ ] 状态类正确实现 ==hashCode(所有字段包含在比较中)
  • [ ] 机制在整个项目中保持一致——手动重写、Equatablefreezed、Dart 记录或其他
  • [ ] 状态对象内的集合不以原始可变 List/Map 形式暴露

响应式纪律(适用于响应式变更方案:MobX、GetX、Signals):

  • [ ] 状态仅通过方案的响应式 API 修改(MobX 中的 @action、信号的 .value、GetX 中的 .obs)——直接字段修改会绕过变更跟踪
  • [ ] 派生值使用方案的计算机制,而非冗余存储
  • [ ] 反应和释放器正确清理(MobX 中的 ReactionDisposer、Signals 中的 effect 清理)

状态形状设计:

  • [ ] 互斥状态使用密封类型、联合变体或方案内置的异步状态类型(例如 Riverpod 的 AsyncValue)——而非布尔标志(isLoadingisErrorhasData
  • [ ] 每个异步操作将加载、成功和错误建模为不同的状态
  • [ ] 所有状态变体在 UI 中穷尽处理——没有静默忽略的情况
  • [ ] 错误状态携带用于显示的错误信息;加载状态不携带过时数据
  • [ ] 可空数据不用作加载指示器——状态是显式的
// 错误——布尔标志汤允许不可能的状态
class UserState {
  bool isLoading = false;
  bool hasError = false; // isLoading && hasError 是可表示的!
  User? user;
}

// 正确(不可变方法)——密封类型使不可能状态不可表示
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
  final User user;
  const UserLoaded(this.user);
}
class UserError extends UserState {
  final String message;
  const UserError(this.message);
}

// 正确(响应式方法)——可观察枚举 + 数据,通过响应式 API 修改
// enum UserStatus { initial, loading, loaded, error }
// 使用方案的 observable/signal 分别包装状态和数据

重建优化:

  • [ ] 状态消费者 Widget(Builder、Consumer、Observer、Obx、Watch 等)范围尽可能窄
  • [ ] 使用选择器仅在特定字段变化时重建——而非每次状态发射
  • [ ] 使用 const Widget 阻止重建在树中传播
  • [ ] 计算/派生状态通过响应式方式计算,而非冗余存储

订阅与释放:

  • [ ] 所有手动订阅(.listen())在 dispose() / close() 中取消
  • [ ] 不再需要时关闭 StreamController
  • [ ] 定时器在释放生命周期中取消
  • [ ] 优先使用框架管理的生命周期而非手动订阅(声明式构建器优于 .listen()
  • [ ] 在异步回调中调用 setState 前检查 mounted
  • [ ] 在 await 后不使用 BuildContext 而不检查 context.mounted(Flutter 3.7+)——过期的 context 会导致崩溃
  • [ ] 在异步间隙后,不验证 Widget 仍挂载的情况下,不进行导航、弹窗或 Scaffold 消息
  • [ ] BuildContext 绝不存储在单例、状态管理器或静态字段中

局部 vs 全局状态:

  • [ ] 临时 UI 状态(复选框、滑块、动画)使用局部状态(setStateValueNotifier
  • [ ] 共享状态仅提升到必要的高度——不过度全局化
  • [ ] 功能范围的状态在功能不再活跃时正确释放

5. 性能

不必要的重建:

  • [ ] setState() 不在根 Widget 级别调用——局部化状态变更
  • [ ] 使用 const Widget 阻止重建传播
  • [ ] 在独立重绘的复杂子树周围使用 RepaintBoundary
  • [ ] 对于独立于动画的子树,使用 AnimatedBuilder 的 child 参数

Build 中的昂贵操作:

  • [ ] 不在 build() 中对大型集合进行排序、过滤或映射——在状态管理层计算
  • [ ] 不在 build() 中编译正则表达式
  • [ ] MediaQuery.of(context) 的使用是具体的(例如 MediaQuery.sizeOf(context)

图片优化:

  • [ ] 网络图片使用缓存(适合项目的任何缓存方案)
  • [ ] 为目标设备使用适当分辨率的图片(不为缩略图加载 4K 图片)
  • [ ] Image.asset 使用 cacheWidth/cacheHeight 以显示尺寸解码
  • [ ] 为网络图片提供占位符和错误 Widget

懒加载:

  • [ ] 对于大型或动态列表,使用 ListView.builder / GridView.builder 而非 ListView(children: [...])(小型静态列表可使用具体构造函数)
  • [ ] 为大数据集实现分页
  • [ ] 在 Web 构建中为重型库使用延迟加载(deferred as

其他:

  • [ ] 避免在动画中使用 Opacity Widget——使用 AnimatedOpacityFadeTransition
  • [ ] 避免在动画中进行裁剪——预裁剪图片
  • [ ] 不在 Widget 上重写 operator ==——改用 const 构造函数
  • [ ] 谨慎使用内在尺寸 Widget(IntrinsicHeightIntrinsicWidth)(额外布局传递)

6. 测试

测试类型与期望:

  • [ ] 单元测试:覆盖所有业务逻辑(状态管理器、仓库、工具函数)
  • [ ] Widget 测试:覆盖单个 Widget 的行为、交互和视觉输出
  • [ ] 集成测试:端到端覆盖关键用户流程
  • [ ] 黄金测试:对设计关键的 UI 组件进行像素级比较

覆盖率目标:

  • [ ] 业务逻辑行覆盖率目标 80%+
  • [ ] 所有状态转换都有对应的测试(加载→成功、加载→错误、重试等)
  • [ ] 测试边界情况:空状态、错误状态、加载状态、边界值

测试隔离:

  • [ ] 外部依赖(API 客户端、数据库、服务)被模拟或伪造
  • [ ] 每个测试文件只测试一个类/单元
  • [ ] 测试验证行为,而非实现细节
  • [ ] Stub 仅定义每个测试所需的行为(最小化 stub)
  • [ ] 测试用例之间没有共享的可变状态

Widget 测试质量:

  • [ ] 正确使用 pumpWidgetpump 处理异步操作
  • [ ] 适当使用 find.byTypefind.textfind.byKey
  • [ ] 没有依赖时序的脆弱测试——使用 pumpAndSettle 或显式 pump(Duration)
  • [ ] 测试在 CI 中运行,失败阻止合并

7. 无障碍

语义 Widget:

  • [ ] 在自动标签不足时,使用 Semantics Widget 提供屏幕阅读器标签
  • [ ] 对纯装饰性元素使用 ExcludeSemantics
  • [ ] 使用 MergeSemantics 将相关 Widget 合并为单个可访问元素
  • [ ] 图片设置 semanticLabel 属性

屏幕阅读器支持:

  • [ ] 所有交互元素可聚焦并具有有意义的描述
  • [ ] 焦点顺序符合逻辑(遵循视觉阅读顺序)

视觉无障碍:

  • [ ] 文本与背景的对比度 >= 4.5:1
  • [ ] 可点击目标至少 48x48 像素
  • [ ] 颜色不是状态的唯一指示符(同时使用图标/文本)
  • [ ] 文本随系统字体大小设置缩放

交互无障碍:

  • [ ] 没有无操作的 onPressed 回调——每个按钮要么执行操作,要么被禁用
  • [ ] 错误字段建议更正
  • [ ] 用户输入数据时,上下文不会意外变化

8. 平台特定关注点

iOS/Android 差异:

  • [ ] 在适当的地方使用平台自适应 Widget
  • [ ] 正确处理返回导航(Android 返回按钮、iOS 滑动返回)
  • [ ] 通过 SafeArea Widget 处理状态栏和安全区域
  • [ ] 在 AndroidManifest.xmlInfo.plist 中声明平台特定权限

响应式设计:

  • [ ] 使用 LayoutBuilderMediaQuery 实现响应式布局
  • [ ] 断点定义一致(手机、平板、桌面)
  • [ ] 文本在小屏幕上不溢出——使用 FlexibleExpandedFittedBox
  • [ ] 测试横屏方向或显式锁定
  • [ ] Web 特定:支持鼠标/键盘交互,存在悬停状态

9. 安全性

安全存储:

  • [ ] 敏感数据(令牌、凭据)使用平台安全存储(iOS 的 Keychain、Android 的 EncryptedSharedPreferences)
  • [ ] 绝不将秘密存储在明文存储中
  • [ ] 考虑对敏感操作使用生物认证门控

API 密钥处理:

  • [ ] API 密钥不硬编码在 Dart 源代码中——使用 --dart-define、排除在 VCS 外的 .env 文件或编译时配置
  • [ ] 秘密不提交到 git——检查 .gitignore
  • [ ] 对于真正秘密的密钥使用后端代理(客户端不应持有服务器秘密)

输入验证:

  • [ ] 所有用户输入在发送到 API 前进行验证
  • [ ] 表单验证使用正确的验证模式
  • [ ] 没有原始 SQL 或用户输入的字符串插值
  • [ ] 深度链接 URL 在导航前验证和清理

网络安全:

  • [ ] 所有 API 调用强制使用 HTTPS
  • [ ] 对于高安全性应用考虑证书固定
  • [ ] 认证令牌正确刷新和过期
  • [ ] 不记录或打印敏感数据

10. 包/依赖审查

评估 pub.dev 包:

  • [ ] 检查 pub points 分数(目标 130+/160)
  • [ ] 检查 点赞流行度 作为社区信号
  • [ ] 验证发布者在 pub.dev 上是否 已验证
  • [ ] 检查最后发布日期——过时的包(>1 年)存在风险
  • [ ] 查看开放问题和维护者的响应时间
  • [ ] 检查许可证与项目的兼容性
  • [ ] 验证平台支持覆盖你的目标

版本约束:

  • [ ] 对依赖使用 caret 语法(^1.2.3)——允许兼容更新
  • [ ] 仅在绝对必要时锁定精确版本
  • [ ] 定期运行 flutter pub outdated 以跟踪过时依赖
  • [ ] 生产 pubspec.yaml 中没有依赖覆盖——仅用于临时修复,并附有注释/问题链接
  • [ ] 最小化传递依赖数量——每个依赖都是一个攻击面

Monorepo 特定(melos/workspace):

  • [ ] 内部包仅从公共 API 导入——不导入 package:other/src/internal.dart(破坏 Dart 包封装)
  • [ ] 内部包依赖使用工作区解析,而非硬编码的 path: ../../ 相对字符串
  • [ ] 所有子包共享或继承根 analysis_options.yaml

11. 导航与路由

通用原则(适用于任何路由方案):

  • [ ] 一致使用一种路由方法——不混合命令式 Navigator.push 和声明式路由器
  • [ ] 路由参数是类型化的——没有 Map<String, dynamic>Object? 类型转换
  • [ ] 路由路径定义为常量、枚举或生成——代码中没有散落的魔法字符串
  • [ ] 认证守卫/重定向集中化——不在各个屏幕中重复
  • [ ] 为 Android 和 iOS 配置深度链接
  • [ ] 深度链接 URL 在导航前验证和清理
  • [ ] 导航状态可测试——路由变更可在测试中验证
  • [ ] 返回行为在所有平台上正确

12. 错误处理

框架错误处理:

  • [ ] 重写 FlutterError.onError 以捕获框架错误(构建、布局、绘制)
  • [ ] 设置 PlatformDispatcher.instance.onError 以捕获 Flutter 未捕获的异步错误
  • [ ] 为发布模式自定义 ErrorWidget.builder(用户友好而非红屏)
  • [ ] 在 runApp 周围包裹全局错误捕获(例如 runZonedGuarded、Sentry/Crashlytics 包装器)

错误报告:

  • [ ] 集成错误报告服务(Firebase Crashlytics、Sentry 或等效服务)
  • [ ] 报告非致命错误并附带堆栈跟踪
  • [ ] 将状态管理错误观察器连接到错误报告(例如 BlocObserver、ProviderObserver 或方案的等效组件)
  • [ ] 将用户可识别信息(用户 ID)附加到错误报告以进行调试

优雅降级:

  • [ ] API 错误导致用户友好的错误 UI,而非崩溃
  • [ ] 对临时网络故障提供重试机制
  • [ ] 优雅处理离线状态
  • [ ] 状态管理中的错误状态携带用于显示的错误信息
  • [ ] 原始异常(网络、解析)在到达 UI 前映射为用户友好的、本地化的消息——绝不向用户显示原始异常字符串

13. 国际化(l10n)

设置:

  • [ ] 配置本地化方案(Flutter 内置的 ARB/l10n、easy_localization 或等效方案)
  • [ ] 在应用配置中声明支持的语言环境

内容:

  • [ ] 所有用户可见的字符串使用本地化系统——Widget 中没有硬编码字符串
  • [ ] 模板文件包含给翻译人员的描述/上下文
  • [ ] 对复数、性别、选择使用 ICU 消息语法
  • [ ] 占位符定义类型
  • [ ] 各语言环境之间没有缺失的键

代码审查:

  • [ ] 在整个项目中一致使用本地化访问器
  • [ ] 日期、时间、数字和货币格式是语言环境感知的
  • [ ] 如果目标语言为阿拉伯语、希伯来语等,支持文本方向(RTL)
  • [ ] 不对本地化文本进行字符串拼接——使用参数化消息

14. 依赖注入

原则(适用于任何 DI 方法):

  • [ ] 类依赖抽象(接口),而非层边界处的具体实现
  • [ ] 依赖通过构造函数、DI 框架或提供者图从外部提供——而非内部创建
  • [ ] 注册区分生命周期:单例 vs 工厂 vs 懒加载单例
  • [ ] 环境特定绑定(开发/预发布/生产)使用配置,而非运行时 if 检查
  • [ ] DI 图中没有循环依赖
  • [ ] 服务定位器调用(如果使用)不散布在业务逻辑中

15. 静态分析

配置:

  • [ ] 存在 analysis_options.yaml 并启用严格设置
  • [ ] 严格分析器设置:strict-casts: truestrict-inference: truestrict-raw-types: true
  • [ ] 包含全面的 lint 规则集(very_good_analysis、flutter_lints 或自定义严格规则)
  • [ ] monorepo 中的所有子包继承或共享根分析选项

执行:

  • [ ] 提交的代码中没有未解决的分析器警告
  • [ ] lint 抑制(// ignore:)有注释说明原因
  • [ ] flutter analyze 在 CI 中运行,失败阻止合并

无论使用哪个 lint 包都要验证的关键规则:

  • [ ] prefer_const_constructors——Widget 树中的性能
  • [ ] avoid_print——使用适当的日志记录
  • [ ] unawaited_futures——防止即发即忘的异步错误
  • [ ] prefer_final_locals——变量级别的不可变性
  • [ ] always_declare_return_types——显式契约
  • [ ] avoid_catches_without_on_clauses——特定错误处理
  • [ ] always_use_package_imports——一致的导入风格

状态管理快速参考

下表将通用原则映射到流行方案中的实现。使用此表将审查规则适配到项目使用的任何方案。

原则 BLoC/Cubit Riverpod Provider GetX MobX Signals 内置
状态容器 Bloc/Cubit Notifier/AsyncNotifier ChangeNotifier GetxController Store signal() StatefulWidget
UI 消费者 BlocBuilder ConsumerWidget Consumer Obx/GetBuilder Observer Watch setState
选择器 BlocSelector/buildWhen ref.watch(p.select(...)) Selector N/A computed computed() N/A
副作用 BlocListener ref.listen Consumer 回调 ever()/once() reaction effect() 回调
释放 通过 BlocProvider 自动 .autoDispose 通过 Provider 自动 onClose() ReactionDisposer 手动 dispose()
测试 blocTest() ProviderContainer 直接 ChangeNotifier 测试中 Get.put 直接 store 直接 signal Widget 测试

来源