关于 flutter-conventions-index
flutter-conventions-index 是 Flutter/Dart 应用开发的入口技能,汇总通用架构与编码规则,并将实现、测试和发布等任务指向专门技能。
主要功能
- 01按功能划分的分层架构规则
- 02Riverpod 状态与依赖注入约定
- 03UI、异步与无障碍指南
- 04按任务索引专门技能
- 05功能实现步骤
适用场景
- 01查找任务适用的技能
- 02开始实现新功能
- 03审查全应用 Flutter 约定
- 04规划测试与发布工作
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ zakariaf/ flutter-skills --skill flutter-conventions-index安装后在 Claude Code 中输入/flutter-conventions-index确认已加载。
详细介绍
Flutter 约定 — 索引
这是 Flutter/Dart 应用的入口。本文说明每项任务都必须遵守的跨领域基本规则,并将每个关注点引导至相应的专项技能。对于任何非简单任务:先遵循本文规则,再查阅相关专项技能以了解详细内容。这是本技能库中唯一列出所有其他技能的技能。
默认假设这是一个单包 Flutter 应用。Monorepo / pub-workspace 指南仅在负责相关内容的技能中说明(project-structure-and-packages、codegen-and-toolchain),小型应用无需遵循。
不容妥协的规则
- 以功能为先、分层、仅向下依赖。 按功能(文件夹)分组,再按层分组:视图 → ViewModel → Repository → Service/数据。下层永不向上导入;依赖图必须是严格的 DAG。原因:无环且向下的依赖图是防止代码变成一团乱麻的唯一结构性保障。(
flutter-architecture、project-structure-and-packages) - Widget 保持简单。 Widget 中不包含业务逻辑、数据访问、数学运算或格式化逻辑——它读取状态并进行渲染。原因:
build()中的逻辑无法测试,且重建行为不可预测。(widget-composition) - 每个屏幕对应一个 ViewModel,管理不可变状态。 单个
Notifier/AsyncNotifier持有私有可变状态,并将其作为具有值相等性的不可变值公开;状态转换时赋予新状态,绝不原地修改。原因:不可变值加单一所有者,让每次变更都可比较、可测试。(state-management-riverpod) - Riverpod 3.x 同时负责状态管理和 DI。 仅使用现代
Notifier/AsyncNotifier/Future/Streamprovider;provider 即 DI 容器。不使用get_it、package:provider,也不使用旧版StateProvider/StateNotifierProvider/ChangeNotifierProvider。原因:只采用一种组合模型,无需协调第二套 DI 框架。(state-management-riverpod、app-startup-and-bootstrap) - 单一写入路径。 Widget 或 ViewModel 绝不直接修改持久化状态;每次修改都通过 Repository 方法完成,先持久化,再通过 stream 重新发布。原因:通过单一、持久且可观测的路径,确保状态不会只写入一半。(
state-management-riverpod、persistence-drift) - 派生而不存储;依赖抽象。 读取时计算派生值,而不是缓存第二份副本;面向注入的接口编程,而不是面向具体实现。原因:重复的数据源会逐渐失去同步;注入的接缝能让代码可测试。(
flutter-architecture、service-boundary-and-native) - 不可变模型,类型化错误。 Domain 和 UI 状态使用
freezed/sealed 值类型,提供copyWith和值相等性。纯函数必须是全函数——返回不确定性,绝不抛出异常。可恢复的 I/O 返回 sealed 类型Result<T, F extends Failure>,并通过穷尽式分支处理;绝不使用会吞掉错误的catch (_) {}。原因:编译器会强制处理所有情况;失败携带稳定的代码,而非本地化字符串。(error-handling-typed-results、dart3-idioms-and-coding-standards) - 通过注入的接口隔离副作用。 所有平台/原生副作用(时钟、通知、分享、分析、存储)都通过 Dart 接口实现,由 provider 提供,在组合根处统一覆盖,并在测试中使用伪实现。通过注入的
Clock读取当前时间,绝不调用DateTime.now()。原因:全局副作用是不可确定、无法测试的依赖。(service-boundary-and-native、value-objects-money-and-units) - 异步操作绝不悄无声息。 对所有操作使用
await,或显式处理Future;不使用即发即弃的箭头回调。每次await后检查BuildContext/mounted;释放控制器、订阅、计时器和 sink。原因:被丢弃的Future会吞掉任何 lint 都检测不到的错误。(async-safety) - 保持单元精简,提取 Widget 类。 遵守
dart3-idioms-and-coding-standards规定的复杂度上限表(方法 ≤30、build()≤80、文件 ≤300、位置参数 ≤3、逻辑嵌套 ≤3——Widget 构建树可按规定的例外嵌套至 ≤5)。提取constStatelessWidget类,绝不使用_buildX()方法;凡允许之处都使用const。原因:小型的 const 子树重建更少,阅读也更快。(dart3-idioms-and-coding-standards、widget-composition、flutter-performance) - 名称体现职责。 使用
…Screen/…Notifier/…Repository/…Service/…Failure;严格遵循 Effective Dart 的大小写规范;使用完整单词,并在名称中注明单位;文件名应与其主要声明一致。原因:通过 grep 搜索或查看文件名,就应能看出所属层级。(naming-conventions) - 从设计上支持 RTL 和无障碍访问。 仅使用方向性(
start/end)几何属性,绝不硬编码左/右;所有用户可见字符串都通过gen_l10n从 ARB 文件获取;为每个语义节点标注标签和角色;绝不限制文本缩放,也不单靠颜色传达信息。原因:正确性属性很容易从一开始就融入设计,却很难事后补上。(i18n-rtl-l10n、accessibility-as-code) - 测试代码结构,而非固定比例。 纯核心逻辑:使用注入时钟的快速
package:test测试和属性不变量。其他所有内容:使用flutter_test和mocktail、ProviderContainer.test()和overrideWith——伪造 Repository/Service,而不是Notifier。通过一个验收测试覆盖整个应用。原因:测试应针对风险;不变量由纯核心逻辑承担。(testing-strategy、widget-golden-and-a11y-testing) - 严格 lint 是底线;格式不可妥协。 每次 PR 前,确保
dart format无问题且dart analyze --fatal-infos通过,并使用版本固定且包含strict-casts/strict-raw-types的very_good_analysis配置。原因:分析器通过是你能获得的成本最低的正确性信号。(lint-and-style-config、dependency-hygiene)
前往正确的技能
| 当你需要…… | 打开 |
|---|---|
| 了解概况,不确定哪个技能适用于当前任务 | flutter-conventions-index(本技能) |
| 决定分层、功能与包的划分、依赖 DAG | flutter-architecture |
搭建包、barrel、lib/ 与 lib/src/、工作区布局 | project-structure-and-packages |
编写 main()、错误处理器、DI 覆盖项、预热,以及应用生命周期中的后台/恢复时刷新 | app-startup-and-bootstrap |
编写 Notifier/AsyncNotifier、family/autoDispose、单一写入路径 | state-management-riverpod |
构建或重构 UI、编写 build()、布局/内边距 | widget-composition |
| 配置应用路由器、重定向/身份验证守卫、深层链接、导航 Shell、过渡效果、PopScope、404 | navigation-and-routing |
构建 Form、同步/异步字段验证、焦点遍历、键盘操作、输入格式化器 | forms-and-input |
| 呈现加载/空/错误状态、Snackbar、横幅、对话框、撤销、重试 | ui-states-and-feedback |
响应式断点、大屏幕/平板/折叠屏主从布局、按屏幕宽度选择 NavigationRail 或 BottomNavigationBar | adaptive-layout |
| 选择 Dart 3 语言结构(sealed、records、类修饰符) | dart3-idioms-and-coding-standards |
| 为类/文件/变量/布尔值命名 | naming-conventions |
配置 analysis_options.yaml、Lint 严重级别 | lint-and-style-config |
| 修复卡顿、缩小重建范围、使用惰性列表、在 Isolate 外执行工作 | flutter-performance |
为公共 API 编写 /// 文档注释 | dartdoc-conventions |
编写异步代码、保护 context/mounted、处理释放 | async-safety |
对错误建模、使用 Result/Failure、确保数据不会丢失的流程 | error-handling-typed-results |
| 编写单元/Widget 测试、设置 Fake | testing-strategy |
| 编写 Golden、布局、RTL 或无障碍 Widget 测试 | widget-golden-and-a11y-testing |
添加 Drift 表、DAO 或 .watch 流 | persistence-drift |
| 编写只向前的架构迁移 | run-migration |
| 导出、备份、分享、导入或恢复用户数据 | data-export-and-restore |
设置 build_runner、build.yaml、生成代码策略 | codegen-and-toolchain |
| 在 analyze 前运行代码生成 | run-codegen |
| 编写 GitHub Actions CI 流水线和门禁 | ci-pipeline-and-gates |
| 添加 ARB 字符串、ICU 复数形式、RTL 几何布局 | i18n-rtl-l10n |
编写 Semantics、目标区域、遍历顺序、文本缩放 | accessibility-as-code |
| 在纯核心中对金额、单位、日期建模 | value-objects-money-and-units |
| 构建设备端提醒/通知 | local-notifications-scheduler |
编写 CustomPainter + 手势命中测试 | custom-canvas-and-gestures |
| 添加动画、触觉反馈、庆祝效果或成功/错误时刻 | motion-and-haptics |
| 根据种子或日期生成内容,确保所有设备上的结果完全一致 | seeded-determinism-and-golden-vectors |
| 添加广告、激励获取循环、付费墙或权益门禁 | ads-and-iap-monetization |
| 接入原生通道或平台副作用 | service-boundary-and-native |
编辑 pubspec/lockfile、审查新依赖 | dependency-hygiene |
| 从头到尾搭建完整的功能模块 | scaffold-feature-module |
| 规划令牌 → 主题 → 组件(不含美学数值) | design-system-structure |
| 重新基准化已提交的 Golden 图像 | run-goldens-rebaseline |
| 发布前进行每个应用仅一次的设计/QA 审查 | design-review-workflow |
| 准备发布:版本控制、签名、符号文件、商店声明、分阶段推出 | release-and-store-shipping |
构建功能时的推荐顺序
- 模型 + 纯函数 — 不可变值类型;全定义、由时钟注入的核心 →
dart3-idioms-and-coding-standards、value-objects-money-and-units - 数据层 — 仓库 = 唯一事实来源 / 唯一写入路径;需要持久化时使用 Drift 表 + DAO →
persistence-drift、error-handling-typed-results - 服务边界 — 每项副作用都通过注入的接口实现 →
service-boundary-and-native - ViewModel —
Notifier/AsyncNotifier、family/autoDispose、意图方法 →state-management-riverpod - 视图 + 小部件 — 简单的
ConsumerWidget、小型const小部件类、表单、响应式布局、适配 RTL 的几何布局;在认为主流程已完成之前,先处理加载 / 空 / 错误状态 →widget-composition、ui-states-and-feedback、forms-and-input、adaptive-layout、i18n-rtl-l10n、accessibility-as-code - 接入 DI + 路由 — 在组合根中覆盖 providers;功能路由注册到唯一的
go_router→app-startup-and-bootstrap、navigation-and-routing、scaffold-feature-module - 文档 + 测试 — 为公共接口编写
///;使用伪对象测试核心不变量 + 容器 / 小部件 →dartdoc-conventions、testing-strategy、widget-golden-and-a11y-testing - 性能分析,然后通过 CI 门禁 — 在 profile 模式下测量;格式化 / 分析 / 代码生成 / 测试全部通过 →
flutter-performance、codegen-and-toolchain、ci-pipeline-and-gates - 评审,然后发布 — 在构建结束时进行一次设计 / QA 检查,然后准备发布产物:版本管理、签名、归档符号、商店声明、分阶段发布 →
design-review-workflow、release-and-store-shipping
全程遵循:naming-conventions 和 lint-and-style-config,确保每一行代码都规范。
基线项目设置
# analysis_options.yaml
include: package:very_good_analysis/analysis_options.yaml # pin the version in pubspec
analyzer:
language:
strict-casts: true
strict-raw-types: true
# The PR gate (mirror in CI — see ci-pipeline-and-gates)
dart run build_runner build --delete-conflicting-outputs # if the app uses codegen
dart format --set-exit-if-changed .
dart analyze --fatal-infos --fatal-warnings
flutter test
每个功能都遵循的结构
// Repository = the SINGLE WRITE PATH: persist first, then observers re-emit.
class OrderRepository {
OrderRepository(this._db, this._clock);
final AppDatabase _db;
final Clock _clock;
/// The only route an order reaches storage. Returns a typed Result, never throws.
Future<Result<void, PersistFailure>> place(OrderDraft draft) async {
try {
await _db.transaction(() async {
await _db.orderDao.insert(draft.toRow(_clock.now())); // durable FIRST
});
return const Ok(null); // watchers over the DAO re-emit on commit
} on DriftException catch (e, s) {
return Err(PersistFailure.write(cause: e, stack: s));
}
}
}
// ViewModel = one StreamNotifier per screen; state tracks EVERY DAO emission.
// Riverpod 3.x: extend the unified base class, opt into autoDispose on the provider.
class OrderListNotifier extends StreamNotifier<List<Order>> {
@override
Stream<List<Order>> build() =>
ref.watch(orderRepositoryProvider).recent(); // re-emits on every commit
Future<void> place(OrderDraft draft) async {
final result = await ref.read(orderRepositoryProvider).place(draft);
switch (result) {
case Ok(): break; // the watched stream re-emits the new state
case Err(:final failure): state = AsyncError(failure, StackTrace.current);
}
}
}
final orderListProvider =
StreamNotifierProvider.autoDispose<OrderListNotifier, List<Order>>(
OrderListNotifier.new,
);
// View = dumb ConsumerWidget: reads one notifier, renders, no logic.
class OrderListScreen extends ConsumerWidget {
const OrderListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ref.watch(orderListProvider).when(
loading: () => const _OrdersSkeleton(),
error: (e, _) => _RetryView(onRetry: () => ref.invalidate(orderListProvider)),
data: (orders) => _OrderListView(orders: orders),
);
}
}
反模式
- 在小部件中进行数据访问或业务逻辑处理,编写臃肿的
build(),或使用_buildXxx()辅助方法 — 难以测试,也不利于重建。 - 使用
setState管理应用 / 共享状态;由 UI 编辑可变模型 — 违反单一所有者 + 不可变状态规则。 - 使用
get_it/package:provider/旧版StateProvider/StateNotifierProvider/ChangeNotifierProvider— 引入第二套 DI / 状态框架,增加协调成本。 - 由小部件或 ViewModel 直接写入 DAO,或采用“重新发布后再持久化” — 绕过唯一写入路径,可能暴露只写入一半的状态。
- 在领域代码中使用
DateTime.now()— 这是一项非确定性依赖;应注入Clock。 - 吞掉错误(
catch (_) {})、让无类型异常泄漏到 UI,或启动一个不等待结果的Future— 这类静默故障没有 lint 能捕获。 - 硬编码左右方向,使用原始数字字符串,或在小部件中写入面向用户的字面文本 — 从设计上就无法适配 RTL 和本地化。
- 使用
!/late/dynamic来规避严格的类型约束;一概使用// ignore;对抗dart format— 这些做法会掩盖真正的问题。 - 未经性能分析就优化,或在 debug 模式下进行性能分析 — 测量的对象不对。
完成定义
- 代码位于正确的功能/层中;依赖图只指向下层(规则 1、6)。
- View 保持轻量;由一个
Notifier/AsyncNotifier管理不可变状态;不使用旧版 Provider 或额外的 DI 容器(规则 2–4)。 - 所有变更都通过仓库的唯一写入路径进行——先持久化,再重新发布(规则 5)。
- 模型不可变且支持值相等;纯函数完备;可恢复的 I/O 使用
Result+ 密封的Failure,并通过穷尽式 switch 处理;不吞掉错误(规则 7)。 - 副作用通过注入的接口实现;“当前时间”来自
Clock(规则 8)。 - 异步操作均已等待/处理;await 后检查
context/mounted;控制器和订阅均已释放(规则 9)。 - 单元符合限制;提取 Widget 类;应用
const;名称体现角色和单位(规则 10–11)。 - 几何布局支持 RTL;字符串放在 ARB 中;语义标签齐全;不限制文本缩放(规则 12)。
- 公共接口有
///文档;核心不变量以及容器/Widget 测试均使用 fake 并通过(规则 13)。 - 在固定的 lint 配置文件中,
dart format+dart analyze --fatal-infos均无问题(规则 14)。 - 已根据任务深度查阅合适的专项技能。
相关技能
此表中的每项技能都是同级技能;请打开路由表指向的技能。从 flutter-architecture 开始,了解代码应放在哪里;再看 state-management-riverpod,了解状态如何流转;最后看 error-handling-typed-results,了解 Result/Failure 主干。
参考资料
- Effective Dart — https://dart.dev/effective-dart
- Flutter 架构建议 — https://docs.flutter.dev/app-architecture
- Riverpod — https://riverpod.dev
- very_good_analysis — https://pub.dev/packages/very_good_analysis
- Flutter 国际化 — https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization
评论
支持 Markdown