Dart 代码审查最佳实践 是做什么的?
一份全面且库无关的清单,用于审查 Flutter/Dart 应用程序。这些原则适用于任何状态管理解决方案、路由库或 DI 框架。
1. 项目整体健康度
- 项目遵循一致的文件夹结构(按功能或按层级)
- 职责分离合理:UI、业务逻辑、数据层
- Widget 中不包含业务逻辑;Widget 纯粹用于呈现
-
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 - 空安全误用:过度使用
!(非空断言运算符)而不用正确的空检查或 Dart 3 模式匹配(if (value case var v?)) - 类型提升失败:使用
this.field而本地变量提升已可工作 - 异常捕获过于宽泛:
catch (e)未使用on子句;始终指定异常类型 - 捕获
Error:Error子类型表示程序错误,不应被捕获 - 未使用的
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. Widget 最佳实践
Widget 分解:
- 单个 Widget 的
build()方法不应超过约 80-100 行 - Widget 应同时根据封装性和变化方式(重建边界)进行拆分
- 返回 Widget 的私有
_build*()辅助方法应抽取为单独的 Widget 类(以启用元素复用、const 传播和框架优化) - 在不需要可变本地状态时,优先使用 StatelessWidget 而非 StatefulWidget
- 可复用的抽取 Widget 应放在单独的文件中
Const 使用:
- 尽可能使用
const构造函数——防止不必要的重建 - 对于不变的集合,使用
const字面量(const []、const {}) - 当所有字段都是 final 时,构造函数应声明为
const
Key 使用:
- 在列表/网格中使用
ValueKey以在重新排序时保持状态 - 谨慎使用
GlobalKey——仅在确实需要跨 Widget 树访问状态时使用 - 避免在
build()中使用UniqueKey——它会强制每帧重建 - 当标识基于数据对象而非单个值时,使用
ObjectKey
主题与设计系统:
- 颜色来自
Theme.of(context).colorScheme——不使用硬编码的Colors.red或十六进制值 - 文本样式来自
Theme.of(context).textTheme——不使用内联的TextStyle和原始字体大小 - 已验证深色模式兼容性——不假设背景为浅色
- 间距和尺寸使用一致的设计 Token 或常量,而非魔法数字
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——优先使用共享仓库或呈现层协调
- 在其他解决方案中:遵循组件间通信的文档约定
- 在 Riverpod 中:provider 通过
不可变性与值相等性(针对不可变状态解决方案:BLoC、Riverpod、Redux):
- 状态对象是不可变的——通过
copyWith()或构造函数创建新实例,绝不在原位置修改 - 状态类正确实现了
==和hashCode(所有字段包含在比较中) - 机制在项目中保持一致——手动重写、
Equatable、freezed、Dart 记录或其他 - 状态对象内的集合不会以原始可变
List/Map形式暴露
响应式纪律(针对响应式变更解决方案:MobX、GetX、Signals):
- 状态仅通过该解决方案的响应式 API(MobX 中的
@action、信号上的.value、GetX 中的.obs)进行变更——直接字段修改会绕过变更追踪 - 派生值使用该解决方案的计算机制,而非冗余存储
- 反应和释放器正确清理(MobX 中的
ReactionDisposer、Signals 中的 effect 清理)
状态形状设计:
- 互斥状态使用密封类型、联合变体或解决方案内置的异步状态类型(例如 Riverpod 的
AsyncValue)——而非布尔标志位(isLoading、isError、hasData) - 每个异步操作将加载、成功和错误建模为不同的状态
- 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 }
// 使用解决方案的可观察量/信号将状态和数据分开包装
重建优化:
- 状态消费者 Widget(Builder、Consumer、Observer、Obx、Watch 等)的作用域尽可能窄
- 使用选择器仅在特定字段更改时重建——而非每次状态发射都重建
- 使用
constWidget 阻止重建沿 Widget 树传播 - 计算/派生状态通过响应式计算,而非冗余存储
订阅与释放:
- 所有手动订阅(
.listen())在dispose()/close()中取消 - 不再需要时关闭 StreamController
- 定时器在释放生命周期中取消
- 优先使用框架管理的生命周期而非手动订阅(声明式构建器优于
.listen()) - 在异步回调中
setState前检查mounted - 在
await后不使用BuildContext,除非检查了context.mounted(Flutter 3.7+)——过时的上下文会导致崩溃 - 在异步间隙后,未验证 Widget 仍挂载前,不执行导航、对话框或 SnackBar 消息
-
BuildContext绝不存储在单例、状态管理器或静态字段中
局部状态与全局状态:
- 临时 UI 状态(复选框、滑块、动画)使用局部状态(
setState、ValueNotifier) - 共享状态仅提升到必要的高度——不过度全局化
- 功能范围的状态在功能不再活跃时正确释放
5. 性能
不必要的重建:
- 不在根 Widget 级别调用
setState()——将状态更改限定在局部 - 使用
constWidget 阻止重建传播 - 在独立重绘的复杂子树周围使用
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)
其他:
- 动画中避免使用
OpacityWidget——使用AnimatedOpacity或FadeTransition - 动画中避免裁剪——预裁剪图片
- 不在 Widget 上重写
operator ==——使用const构造函数代替 - 谨慎使用内在尺寸 Widget(
IntrinsicHeight、IntrinsicWidth)(会额外增加布局计算)
6. 测试
测试类型与期望:
- 单元测试:覆盖所有业务逻辑(状态管理器、仓库、工具函数)
- Widget 测试:覆盖单个 Widget 的行为、交互和视觉输出
- 集成测试:端到端覆盖关键用户流程
- 快照测试:对设计关键的 UI 组件进行像素级精确对比
覆盖率目标:
- 业务逻辑行覆盖率力争达到 80% 以上
- 所有状态转换都有对应的测试(加载→成功、加载→错误、重试等)
- 测试边缘情况:空状态、错误状态、加载状态、边界值
测试隔离:
- 外部依赖(API 客户端、数据库、服务)被模拟或伪造
- 每个测试文件仅测试一个类/单元
- 测试验证的是行为,而非实现细节
- Stub 仅定义每个测试所需的行为(最小化 Stub)
- 测试用例之间不共享可变状态
Widget 测试质量:
- 正确使用
pumpWidget和pump处理异步操作 - 正确使用
find.byType、find.text、find.byKey - 没有依赖时机的不可靠测试——使用
pumpAndSettle或显式的pump(Duration) - 测试在 CI 中运行,失败会阻止合并
7. 可访问性
语义 Widget:
- 在自动标签不足时,使用
SemanticsWidget 提供屏幕阅读器标签 - 为纯装饰性元素使用
ExcludeSemantics - 使用
MergeSemantics将相关 Widget 合并为单个可访问元素 - 图片设置了
semanticLabel属性
屏幕阅读器支持:
- 所有交互元素都可聚焦,并具有有意义的描述
- 焦点顺序符合逻辑(遵循视觉阅读顺序)
视觉可访问性:
- 文本与背景的对比度比率 >= 4.5:1
- 可点击目标至少为 48x48 像素
- 颜色不是状态的唯一指示符(同时使用图标/文本)
- 文本随系统字体大小设置缩放
交互可访问性:
- 无空操作
onPressed回调——每个按钮都有具体操作或被禁用 - 错误字段建议更正
- 用户输入数据时,上下文不会意外变化
8. 平台特定问题
iOS/Android 差异:
- 在适当位置使用平台自适应 Widget
- 正确处理返回导航(Android 返回按钮、iOS 滑动返回)
- 通过
SafeAreaWidget 处理状态栏和安全区域 - 平台特定权限已在
AndroidManifest.xml和Info.plist中声明
响应式设计:
- 使用
LayoutBuilder或MediaQuery实现响应式布局 - 断点定义一致(手机、平板、桌面)
- 文本在小屏幕上不溢出——使用
Flexible、Expanded、FittedBox - 横屏模式已测试或已明确锁定
- Web 特定:支持鼠标/键盘交互,存在悬停状态
9. 安全
安全存储:
- 敏感数据存储在安全存储中(如
flutter_secure_storage),而非 SharedPreferences 或明文文件 - 令牌和密钥不在代码中硬编码——使用环境变量或安全配置服务
- 本地缓存中的敏感数据在注销时被清除
网络通信:
- 所有 API 调用使用 HTTPS
- 证书固定已考虑用于高安全性场景
- API 密钥或令牌不在日志或错误报告中泄露
- 敏感端点的请求/响应未在调试工具外记录
输入验证:
- 表单输入已针对类型、长度和格式进行验证
- 验证规则在客户端和服务器端都适用
- 用户输入在显示前被转义,以防注入攻击
10. 整洁架构与依赖注入
依赖注入:
- 依赖通过 DI 容器或构造函数注入传递——不在类内部实例化
- 生命周期管理适当(单例、作用域实例、瞬态)
- 测试中的模拟通过 DI 轻松替换,无需修改生产代码
- DI 配置集中且清晰
整洁架构层级:
- 演示层(Widget、页面)不包含领域实体或数据模型
- 领域层是纯粹的 Dart,无框架依赖——包含实体、用例和仓库接口
- 数据层实现仓库接口,处理 JSON 序列化/反序列化
- 层之间的依赖指向内部(领域层不依赖于数据层)
- 数据传输对象(DTO)/模型不会泄露到表示层——它们被映射到领域实体
这份清单来自 Flutter/Dart 代码审查最佳实践。