技能 编程开发 Flutter/Dart 代码审查指南

Flutter/Dart 代码审查指南

v20260330
flutter-dart-code-review
一个与库无关的 Flutter/Dart 代码审查清单,覆盖部件实践、状态管理、Dart 习惯用法、性能、可访问性、安全与整洁架构,帮助审查者快速识别问题。
获取技能
474 次下载
概览

Flutter/Dart 代码审查最佳实践

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


1. 通用项目健康度

  • [ ] 项目遵循一致的文件夹结构(功能优先或分层优先)
  • [ ] 关注点分离得当:UI、业务逻辑、数据层
  • [ ] 部件中无业务逻辑;部件纯粹是展示性的
  • [ ] pubspec.yaml 是干净的 —— 没有未使用的依赖项,版本已适当固定
  • [ ] analysis_options.yaml 包含严格的 lint 规则集,并启用了严格的分析器设置
  • [ ] 生产代码中没有 print() 语句 —— 使用 dart:developer log() 或日志包
  • [ ] 生成的文件 (.g.dart, .freezed.dart, .gr.dart) 是最新的或在 .gitignore
  • [ ] 平台特定代码通过抽象进行隔离

2. Dart 语言陷阱

  • [ ] 隐式动态类型:缺少类型注解导致 dynamic —— 启用 strict-casts, strict-inference, strict-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() 来表明意图
  • [ ] final 可用时使用 var:局部变量首选 final,编译时常量首选 const
  • [ ] 相对导入:为保持一致性,使用 package: 导入
  • [ ] 暴露可变集合:公共 API 应返回不可修改的视图,而不是原始的 List/Map
  • [ ] 缺少 Dart 3 模式匹配:优先使用 switch 表达式和 if-case,而不是冗长的 is 检查和手动类型转换
  • [ ] 为多重返回值使用一次性类:使用 Dart 3 记录 (String, int) 代替一次性 DTO
  • [ ] 生产代码中的 print():使用 dart:developer log() 或项目的日志包;print() 没有日志级别且无法过滤

3. 部件最佳实践

部件分解:

  • [ ] 没有单个部件的 build() 方法超过约 80-100 行
  • [ ] 部件按封装方式以及按变化方式(重建边界)进行拆分
  • [ ] 返回部件的私有 _build*() 辅助方法被提取到单独的部件类中(支持元素重用、常量传播和框架优化)
  • [ ] 在不需要可变局部状态的地方,优先使用无状态部件而非有状态部件
  • [ ] 提取的部件在可复用时放在单独的文件中

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 等)。

架构:

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

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

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

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

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

状态形状设计:

  • [ ] 互斥状态使用密封类型、联合变体或解决方案内置的异步状态类型(例如 Riverpod 的 AsyncValue)—— 而不是布尔标志 (isLoading, isError, hasData)
  • [ ] 每个异步操作都将加载、成功和错误建模为不同的状态
  • [ ] UI 中详尽处理所有状态变体 —— 没有静默忽略的情况
  • [ ] 错误状态携带用于显示的错误信息;加载状态不携带陈旧数据
  • [ ] 可空数据不用于作为加载指示器 —— 状态是明确的
// BAD — boolean flag soup allows impossible states
class UserState {
  bool isLoading = false;
  bool hasError = false; // isLoading && hasError is representable!
  User? user;
}

// GOOD (immutable approach) — sealed types make impossible states unrepresentable
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);
}

// GOOD (reactive approach) — observable enum + data, mutations via reactivity API
// enum UserStatus { initial, loading, loaded, error }
// Use your solution's observable/signal to wrap status and data separately

重建优化:

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

订阅与清理:

  • [ ] 所有手动订阅 (.listen()) 在 dispose() / close() 中被取消
  • [ ] 流控制器在不再需要时关闭
  • [ ] 定时器在清理生命周期中被取消
  • [ ] 优先使用框架管理的生命周期,而不是手动订阅(声明式构建器优于 .listen()
  • [ ] 异步回调中在 setState 之前检查 mounted
  • [ ] 在 await 之后使用 BuildContext 而不检查 context.mounted(Flutter 3.7+)—— 过时的上下文会导致崩溃
  • [ ] 在异步间隙后,没有在验证部件仍然挂载的情况下进行导航、显示对话框或脚手架消息
  • [ ] BuildContext 绝不存储在单例、状态管理器或静态字段中

本地状态与全局状态:

  • [ ] 临时 UI 状态(复选框、滑块、动画)使用本地状态 (setState, ValueNotifier)
  • [ ] 共享状态仅提升到所需的高度 —— 不过度全局化
  • [ ] 功能作用域的状态在功能不再活跃时被正确清理

5. 性能

不必要的重建:

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

build() 中的昂贵操作:

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

图像优化:

  • [ ] 网络图像使用缓存(适用于项目的任何缓存解决方案)
  • [ ] 为目标设备使用适当的图像分辨率(不为缩略图加载 4K 图像)
  • [ ] 使用带有 cacheWidth/cacheHeightImage.asset 以按显示尺寸解码
  • [ ] 为网络图像提供占位符和错误部件

懒加载:

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

其他:

  • [ ] 在动画中避免使用 Opacity 部件 —— 使用 AnimatedOpacityFadeTransition
  • [ ] 在动画中避免裁剪 —— 预裁剪图像
  • [ ] 不在部件上重写 operator == —— 使用 const 构造器代替
  • [ ] 固有尺寸部件 (IntrinsicHeight, IntrinsicWidth) 谨慎使用(额外的布局传递)

6. 测试

测试类型与期望:

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

覆盖率目标:

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

测试隔离:

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

小部件测试质量:

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

7. 无障碍功能

语义化小部件:

  • [ ] 使用 Semantics 小部件在自动标签不足时提供屏幕阅读器标签
  • [ ] 使用 ExcludeSemantics 处理纯装饰性元素
  • [ ] 使用 MergeSemantics 将相关小部件组合成单个可访问元素
  • [ ] 图像设置了 semanticLabel 属性

屏幕阅读器支持:

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

视觉无障碍:

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

交互无障碍:

  • [ ] 没有无操作的 onPressed 回调——每个按钮都有作用或处于禁用状态
  • [ ] 错误字段建议更正
  • [ ] 用户输入数据时,上下文不会意外改变

8. 平台特定考量

iOS/Android 差异:

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

响应式设计:

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

9. 安全性

安全存储:

  • [ ] 敏感数据(令牌、凭证)使用平台安全存储存储(iOS 上的 Keychain,Android 上的 EncryptedSharedPreferences)
  • [ ] 从不以明文存储机密信息
  • [ ] 对于敏感操作考虑使用生物识别认证门控

API 密钥处理:

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

输入验证:

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

网络安全:

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

10. 包/依赖项审查

评估 pub.dev 包:

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

版本约束:

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

单仓库特定(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 或等效方案)
  • [ ] 在应用配置中声明了支持的语言环境

内容:

  • [ ] 所有用户可见字符串都使用本地化系统——小部件中没有硬编码字符串
  • [ ] 模板文件包含翻译人员的描述/上下文
  • [ ] 使用 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 或自定义严格规则)
  • [ ] 单仓库中的所有子包继承或共享根分析选项

执行:

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

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

  • [ ] prefer_const_constructors——小部件树中的性能
  • [ ] 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 小部件测试

来源

信息
Category 编程开发
Name flutter-dart-code-review
版本 v20260330
大小 21.88KB
更新时间 2026-03-31
语言