Flutter/Dart 代码审查最佳实践¶
一个不依赖库的全面检查清单,用于审查 Flutter/Dart 应用程序。无论使用哪种状态管理解决方案、路由库或 DI 框架,这些原则都适用。
1. 整体项目健康度¶
- [ ] 项目遵循一致的文件夹结构(功能优先或分层优先)
- [ ] 适当的关注点分离:UI、业务逻辑、数据层
- [ ] 组件中不包含业务逻辑;组件纯粹是展示层
- [ ]
pubspec.yaml保持整洁 — 没有未使用的依赖项,版本已适当固定 - [ ]
analysis_options.yaml包含严格的 lint 集合和严格的分析器配置 - [ ] 生产代码中没有
print()语句 — 使用dart:developer的log()或日志包 - [ ] 生成的文件(
.g.dart、.freezed.dart、.gr.dart)是最新的或包含在.gitignore中 - [ ] 平台特定代码被隔离在抽象之后
2. Dart 语言陷阱¶
- [ ] 隐式 dynamic:缺少类型注解导致
dynamic— 启用strict-casts、strict-inference、strict-raw-types - [ ] 误用空安全:过度使用
!(bang 运算符),而不是适当的空检查或 Dart 3 的模式匹配(if (value case var v?)) - [ ] 类型提升失败:在本地变量提升可以工作的地方使用
this.field - [ ] 捕获过宽的异常:没有
on子句的catch (e);始终指定异常类型 - [ ] 捕获
Error:Error的子类型表示 bug,不应捕获 - [ ] 未使用的
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 []、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(所有字段都包含在比较中) - [ ] 机制在整个项目中保持一致 — 手动覆盖、
Equatable、freezed、Dart 记录或其他 - [ ] 状态对象中的集合没有作为原始可变
List/Map公开
响应性纪律(用于响应式突变解决方案:MobX、GetX、Signals):¶
- [ ] 状态仅通过解决方案的响应式 API 突变(MobX 中的
@action、signal 中的.value、GetX 中的.obs)— 直接字段突变会绕过变更跟踪 - [ ] 派生值使用解决方案的计算机制,而不是冗余存储
- [ ] 反应和释放器被正确清理(MobX 中的
ReactionDisposer、Signals 中的 effect 清理)
状态形状设计:¶
- [ ] 互斥状态使用 sealed 类型、联合变体或解决方案的内置异步状态类型(例如 Riverpod 的
AsyncValue)— 不使用布尔标志(isLoading、isError、hasData) - [ ] 所有异步操作将加载、成功、错误建模为不同状态
- [ ] 所有状态变体在 UI 中全面处理 — 没有静默忽略的情况
- [ ] 错误状态具有用于显示的错误信息;加载状态不包含旧数据
- [ ] 不使用可变数据作为加载指示器 — 状态是明确的
// 悪い例 — ブールフラグの混乱が不可能な状態を許可する
class UserState {
bool isLoading = false;
bool hasError = false; // isLoading && hasErrorが表現可能!
User? user;
}
// 良い例(イミュータブルアプローチ) — sealed型が不可能な状態を表現不可能にする
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);
}
// 良い例(リアクティブアプローチ) — observableのenum + データ、リアクティビティAPIを通じた変異
// enum UserStatus { initial, loading, loaded, error }
// ソリューションのobservable/signalを使用してstatusとdataを別々にラップする
重建优化:¶
- [ ] 尽可能缩小状态消费者组件(Builder、Consumer、Observer、Obx、Watch 等)的作用域
- [ ] 使用选择器,仅在特定字段变化时重建 — 不要在所有状态发出时重建
- [ ] 使用
const组件阻止重建通过树传播 - [ ] 计算/派生状态被响应式计算,而不是冗余存储
订阅和释放:¶
- [ ] 所有手动订阅(
.listen())在dispose()/close()中取消 - [ ] 流控制器在不再需要时关闭
- [ ] 定时器在释放生命周期中取消
- [ ] 优先使用框架管理生命周期,而不是手动订阅(声明式构建器优于
.listen()) - [ ] 异步回调中的
setState之前检查mounted - [ ] 不要在
await后使用BuildContext而不检查context.mounted(Flutter 3.7+) — 旧上下文会导致崩溃 - [ ] 不要在异步间隙后使用导航、对话框或 scaffold 消息,而不确认组件仍然挂载
- [ ] 不要将
BuildContext存储在单例、状态管理器或静态字段中
本地与全局状态:¶
- [ ] 临时 UI 状态(复选框、滑块、动画)使用本地状态(
setState、ValueNotifier) - [ ] 状态仅提升到需要共享的程度 — 不要过度全局化
- [ ] 功能范围的状态在功能不再活动时正确释放
5. 性能¶
不必要的重建¶
- [ ] 未在根小部件级别调用
setState()— 将状态变更局部化 - [ ] 使用
const小部件来阻止重建传播 - [ ] 在独立重绘的复杂子树周围使用
RepaintBoundary - [ ] 将
AnimatedBuilder的 child 参数用于与动画独立的子树
build() 内的高成本操作¶
- [ ]
build()中没有对大型集合进行排序、过滤、映射 — 在状态管理层计算 - [ ]
build()中没有编译正则表达式 - [ ]
MediaQuery.of(context)的使用是具体的(例如:MediaQuery.sizeOf(context))
图像优化¶
- [ ] 网络图像使用缓存(适合项目的缓存解决方案)
- [ ] 图像分辨率适合目标设备(不要为缩略图加载 4K 图像)
- [ ] 使用
Image.asset和cacheWidth/cacheHeight按显示尺寸解码 - [ ] 为网络图像提供占位符和错误小部件
延迟加载¶
- [ ] 对于大型或动态列表,使用
ListView.builder/GridView.builder而不是ListView(children: [...])(对于小型静态列表,具体构造函数是合适的) - [ ] 为大型数据集实现分页
- [ ] 在 Web 构建中对重量级库使用延迟加载(
deferred as)
其他¶
- [ ] 在动画中避免使用
Opacity小部件 — 使用AnimatedOpacity或FadeTransition - [ ] 在动画中避免裁剪 — 预先裁剪图像
- [ ] 不要在小部件中重写
operator ==— 改用const构造函数 - [ ] 谨慎使用内在尺寸小部件(
IntrinsicHeight、IntrinsicWidth)(会增加额外的布局路径)
6. 测试¶
测试类型与预期¶
- [ ] 单元测试:覆盖所有业务逻辑(状态管理器、仓库、工具函数)
- [ ] 小部件测试:覆盖单个小部件的行为、交互和视觉输出
- [ ] 集成测试:端到端覆盖关键用户流程
- [ ] 黄金测试:对设计关键的 UI 组件进行像素级比较
覆盖率目标¶
- [ ] 业务逻辑的行覆盖率目标为 80% 以上
- [ ] 所有状态转换都有对应的测试(加载→成功、加载→错误、重试等)
- [ ] 测试边缘情况:空状态、错误状态、加载状态、边界值
测试隔离¶
- [ ] 外部依赖(API 客户端、数据库、服务)已被模拟或伪造
- [ ] 每个测试文件恰好测试一个类/单元
- [ ] 测试验证行为而不是实现细节
- [ ] 桩只定义每个测试所需的行为(最小化打桩)
- [ ] 测试用例之间没有共享可变状态
小部件测试质量¶
- [ ] 对异步操作正确使用
pumpWidget和pump - [ ] 正确使用
find.byType、find.text、find.byKey - [ ] 没有依赖时序的不稳定测试 — 使用
pumpAndSettle或显式的pump(Duration) - [ ] 测试在 CI 中运行,失败会阻止合并
7. 可访问性¶
语义小部件¶
- [ ] 在自动标签不足的地方,使用
Semantics小部件提供屏幕阅读器标签 - [ ] 对纯装饰性元素使用
ExcludeSemantics - [ ] 使用
MergeSemantics将相关小部件合并为单个可访问元素 - [ ] 为图像设置
semanticLabel属性
屏幕阅读器支持¶
- [ ] 所有可交互元素都可聚焦并具有有意义的说明
- [ ] 焦点顺序合理(遵循视觉阅读顺序)
视觉可访问性¶
- [ ] 文本与背景的对比度至少为 4.5:1
- [ ] 可点击目标至少为 48x48 像素
- [ ] 颜色不是唯一的状态指示(与图标/文本一起使用)
- [ ] 文本会根据系统字体大小设置缩放
交互可访问性¶
- [ ] 没有无操作的
onPressed回调 — 所有按钮要么执行某些操作,要么被禁用 - [ ] 错误字段提供修正建议
- [ ] 用户输入数据期间上下文不会意外改变
8. 平台特定注意事项¶
iOS/Android 差异¶
- [ ] 在适当的地方使用平台自适应小部件
- [ ] 正确处理后退导航(Android 返回按钮、iOS 滑动返回)
- [ ] 状态栏和安全区域由
SafeArea小部件处理 - [ ] 平台特定权限已在
AndroidManifest.xml和Info.plist中声明
响应式设计¶
- [ ] 响应式布局使用
LayoutBuilder或MediaQuery - [ ] 断点定义一致(手机、平板、桌面)
- [ ] 文本在小屏幕上不会溢出 — 使用
Flexible、Expanded、FittedBox - [ ] 横屏模式已测试或被明确锁定
- [ ] Web 特定:支持鼠标/键盘交互,并存在悬停状态
9. 安全¶
安全存储¶
- [ ] 机密数据(令牌、凭据)使用平台安全存储(iOS Keychain、Android EncryptedSharedPreferences)
- [ ] 不在明文存储中保存密钥
- [ ] 考虑为敏感操作添加生物识别门禁
API 密钥处理¶
- [ ] API 密钥未硬编码在 Dart 源代码中 — 使用
--dart-define、从 VCS 排除的.env文件或编译时配置 - [ ] 密钥未被提交到 git — 检查
.gitignore - [ ] 对真正保密的密钥使用后端代理(客户端不应持有服务器密钥)
输入验证¶
- [ ] 所有用户输入在发送到 API 前都经过验证
- [ ] 表单验证使用适当的验证模式
- [ ] 没有对用户输入进行原始 SQL 或字符串插值
- [ ] 导航前对深度链接 URL 进行验证和清理
网络安全¶
- [ ] 所有 API 调用都强制使用 HTTPS
- [ ] 对高安全应用考虑证书固定
- [ ] 认证令牌能够正确刷新和过期
- [ ] 机密数据不会被记录到日志或输出中
10. 包/依赖项审查¶
pub.dev 包评估¶
- [ ] 检查 pub 点数(目标 130+/160)
- [ ] 作为社区信号检查 点赞 和 热度
- [ ] 确认 pub.dev 上的发布者已 认证
- [ ] 检查最后发布日期 — 旧包(超过 1 年)存在风险
- [ ] 检查未解决的问题和维护者的响应时间
- [ ] 确认许可证与项目兼容
- [ ] 确认平台支持覆盖目标
版本约束¶
- [ ] 依赖项使用 caret 语法(
^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?转换 - [ ] 路由路径定义为常量、enum 或生成代码 — 没有散落在代码中的魔法字符串
- [ ] 认证守卫/重定向集中管理 — 不在各个屏幕中重复
- [ ] 深度链接已在 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 框架或提供者图从外部提供 — 不在内部创建
- [ ] 注册区分生命周期:单例、工厂和延迟单例
- [ ] 环境特定绑定(dev/staging/prod)使用配置,而不是运行时
if检查 - [ ] DI 图中没有循环依赖
- [ ] 服务定位器调用(如果使用时)没有散落在整个业务逻辑中
15. 静态分析¶
配置:¶
- [ ]
analysis_options.yaml存在并启用严格设置 - [ ] 严格分析器设置:
strict-casts: true、strict-inference: true、strict-raw-types: true - [ ] 包含全面的 Lint 规则集(very_good_analysis、flutter_lints 或自定义严格规则)
- [ ] Monorepo 内的所有子包继承或共享根解析选项
应用:¶
- [ ] 已提交代码中没有未解决的分析器警告
- [ ] Lint 抑制(
// ignore:)有解释原因的注释加以证明 - [ ]
flutter analyze在 CI 中运行,并且失败会阻止合并
无论使用哪个 Lint 包都应确认的主要规则:¶
- [ ]
prefer_const_constructors— 小部件树性能 - [ ]
avoid_print— 使用适当的日志记录 - [ ]
unawaited_futures— 防止即发即忘异步 bug - [ ]
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 | 小部件测试 |
来源¶
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC