flutter-conventions-index

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Flutter Conventions — Index

Flutter 约定 — 索引

The front door for this Flutter/Dart app. It states the cross-cutting house rules every task obeys, then routes each concern to a focused skill. For any non-trivial task: apply the rules here, then open the specialized skill for depth. This is the only skill that names every other skill in the library.
Assumes a single-package Flutter app by default. Monorepo / pub-workspace guidance is fenced inside the skills that own it (
project-structure-and-packages
,
codegen-and-toolchain
) — never required for a small app.
这是 Flutter/Dart 应用的总入口。它首先规定每项任务都必须遵守的跨领域团队规则,然后将每个关注点分流到专门的 skill。对于任何非简单任务:先应用这里的规则,再打开专业 skill 获取深入指导。这是整个 skill 库中唯一列出所有其他 skill 的 skill。
默认假设为单 package Flutter 应用。Monorepo / pub-workspace 指南限定在负责相关主题的 skill 中(
project-structure-and-packages
、
codegen-and-toolchain
),小型应用无需使用。

Non-negotiable rules

不可妥协的规则

  1. Feature-first, layered, downward-only. Group by feature (a folder), then by layer: View → ViewModel → Repository → Service/data. Lower layers never import upward; the dependency graph is a strict DAG. WHY: an acyclic downward graph is the only structural guard against a big ball of mud. (
    flutter-architecture
    ,
    project-structure-and-packages
    )
  2. Widgets are dumb. No business logic, data access, math, or formatting in a widget — it reads state and renders. WHY: logic in
    build()
    is untestable and rebuilds unpredictably. (
    widget-composition
    )
  3. One ViewModel per screen, over immutable state. A single
    Notifier
    /
    AsyncNotifier
    owns private mutable state and exposes it as an immutable value with value equality; a transition assigns a new state, never mutates in place. WHY: immutable value + single owner makes every change diffable and testable. (
    state-management-riverpod
    )
  4. Riverpod 3.x is state + DI. Modern
    Notifier
    /
    AsyncNotifier
    /
    Future
    /
    Stream
    providers only; providers are the DI container. No
    get_it
    , no
    package:provider
    , no legacy
    StateProvider
    /
    StateNotifierProvider
    /
    ChangeNotifierProvider
    . WHY: one composition model, no second DI framework to reconcile. (
    state-management-riverpod
    ,
    app-startup-and-bootstrap
    )
  5. Single write path. A widget or ViewModel never mutates persisted state directly; every mutation is a repository method that persists first, then republishes via a stream. WHY: one durable, observable route means state is never half-written. (
    state-management-riverpod
    ,
    persistence-drift
    )
  6. Derive, don't store; depend on abstractions. Compute derived values on read instead of caching a second copy; program against interfaces you inject, not concretes. WHY: a duplicated source of truth drifts out of sync; injected seams keep code testable. (
    flutter-architecture
    ,
    service-boundary-and-native
    )
  7. Immutable models, typed errors. Domain and UI state are
    freezed
    /sealed value types with
    copyWith
    and value equality. Pure functions are total — they return uncertainty, never throw. Recoverable I/O returns a sealed
    Result<T, F extends Failure>
    , switched exhaustively; never a swallowed
    catch (_) {}
    . WHY: the compiler enforces every case; failures carry a stable code, not a localized string. (
    error-handling-typed-results
    ,
    dart3-idioms-and-coding-standards
    )
  8. Side effects behind injected interfaces. Every platform/native effect (clock, notifications, share, analytics, storage) is a Dart interface behind a provider, overridden once at the composition root, faked in tests. Read "now" from an injected
    Clock
    , never
    DateTime.now()
    . WHY: a global side effect is a non-deterministic, untestable dependency. (
    service-boundary-and-native
    ,
    value-objects-money-and-units
    )
  9. Async is never silent.
    await
    everything or handle the
    Future
    explicitly; no fire-and-forget arrow callbacks. Guard
    BuildContext
    /
    mounted
    after every
    await
    ; dispose controllers, subscriptions, timers, and sinks. WHY: a dropped
    Future
    swallows errors no lint catches. (
    async-safety
    )
  10. Small units, extracted widget classes. Keep to the complexity-limit table owned by
    dart3-idioms-and-coding-standards
    (method ≤30,
    build()
    ≤80, file ≤300, positional params ≤3, logic nesting ≤3 — widget build trees may nest to ≤5 as the stated exception). Extract
    const
    StatelessWidget
    classes, never
    _buildX()
    methods;
    const
    everywhere legal. WHY: small const subtrees rebuild less and read faster. (
    dart3-idioms-and-coding-standards
    ,
    widget-composition
    ,
    flutter-performance
    )
  11. Names carry roles.
    …Screen
    /
    …Notifier
    /
    …Repository
    /
    …Service
    /
    …Failure
    ; Effective-Dart casing verbatim; full words with units in the name; file named after its primary declaration. WHY: a grep or a filename should reveal the layer. (
    naming-conventions
    )
  12. RTL and a11y by construction. Use directional (
    start
    /
    end
    ) geometry only, never hardcoded left/right; every user string comes from an ARB via
    gen_l10n
    ; label and role every semantic node; never clamp the text scaler or rely on color alone. WHY: correctness properties are cheap to build in and expensive to retrofit. (
    i18n-rtl-l10n
    ,
    accessibility-as-code
    )
  13. Test the shape of the code, not a fixed ratio. Pure core: fast, clock-injected
    package:test
    + property invariants. Everything else:
    flutter_test
    +
    mocktail
    ,
    ProviderContainer.test()
    +
    overrideWith
    — fake the repository/services, not the
    Notifier
    . One acceptance test anchors the whole app. WHY: tests follow risk; the pure core carries the invariants. (
    testing-strategy
    ,
    widget-golden-and-a11y-testing
    )
  14. Strict lint is the floor; format is not negotiable.
    dart format
    clean and
    dart analyze --fatal-infos
    green before every PR, on a version-pinned
    very_good_analysis
    include with
    strict-casts
    /
    strict-raw-types
    . WHY: a green analyzer is the cheapest correctness signal you have. (
    lint-and-style-config
    ,
    dependency-hygiene
    )
  1. 以 feature 为先,分层且只能向下依赖。 先按 feature(文件夹)分组,再按层划分:View → ViewModel → Repository → Service/data。下层绝不向上导入;依赖图必须是严格的 DAG。原因是:无环的向下依赖图是防止代码变成一团乱麻的唯一结构性保障。(
    flutter-architecture
    、
    project-structure-and-packages
    )
  2. Widget 应保持无业务逻辑。 Widget 中不得包含业务逻辑、数据访问、计算或格式化逻辑——它只负责读取状态并进行渲染。原因是:写在
    build()
    中的逻辑难以测试,而且重建行为不可预测。(
    widget-composition
    )
  3. 每个屏幕使用一个 ViewModel,并采用不可变状态。 单个
    Notifier
    /
    AsyncNotifier
    持有私有可变状态,并以具有值相等性的不可变值对外暴露;每次状态转换都赋值一个新状态,绝不原地修改。原因是:不可变值与单一所有者使每次变更都可比较、可测试。(
    state-management-riverpod
    )
  4. Riverpod 3.x 同时负责状态管理与 DI。 仅使用现代的
    Notifier
    /
    AsyncNotifier
    /
    Future
    /
    Stream
    providers;providers 就是 DI 容器。不得使用
    get_it
    、
    package:provider
    ,也不得使用旧版
    StateProvider
    /
    StateNotifierProvider
    /
    ChangeNotifierProvider
    。原因是:只保留一种组合模型,无需再协调第二套 DI 框架。(
    state-management-riverpod
    、
    app-startup-and-bootstrap
    )
  5. 单一写入路径。 Widget 或 ViewModel 绝不直接修改持久化状态;每次变更都通过 repository 方法完成,先持久化,再通过 stream 重新发布。原因是:一条持久且可观察的路径可确保状态永远不会被写入一半。(
    state-management-riverpod
    、
    persistence-drift
    )
  6. 派生而非存储;依赖抽象而非实现。 在读取时计算派生值,而不是缓存第二份副本;面向注入的接口编程,而不是具体实现。原因是:重复的数据源会逐渐失去同步;注入的接缝可保持代码可测试。(
    flutter-architecture
    、
    service-boundary-and-native
    )
  7. 不可变模型与类型化错误。 领域状态和 UI 状态均使用带有
    copyWith
    和值相等性的
    freezed
    /sealed 值类型。纯函数是全函数(total)——返回表示不确定性的结果,绝不抛出异常。可恢复的 I/O 返回 sealed 的
    Result<T, F extends Failure>
    ,并以穷尽方式处理;绝不能吞掉
    catch (_) {}
    中的错误。原因是:编译器可以强制处理每一种情况;Failure 携带稳定的错误代码,而不是本地化字符串。(
    error-handling-typed-results
    、
    dart3-idioms-and-coding-standards
    )
  8. 副作用置于注入式接口之后。 每项平台/native 副作用(时钟、通知、分享、分析、存储)都应实现为 provider 背后的 Dart 接口,在组合根统一覆盖一次,并在测试中替换为 fake。读取“当前时间”时应使用注入的
    Clock
    ,绝不能调用
    DateTime.now()
    。原因是:全局副作用是一种不可预测、不可测试的依赖。(
    service-boundary-and-native
    、
    value-objects-money-and-units
    )
  9. 异步处理绝不能悄无声息。 所有异步操作都要
    await
    ,或显式处理
    Future
    ;不得使用放任执行的箭头回调。每次
    await
    之后都要检查
    BuildContext
    /
    mounted
    ;并释放 controllers、subscriptions、timers 和 sinks。原因是:被丢弃的
    Future
    会吞掉 lint 无法发现的错误。(
    async-safety
    )
  10. 保持小单元,并提取独立的 widget 类。 遵守
    dart3-idioms-and-coding-standards
    所维护的复杂度限制表(方法 ≤30、
    build()
    ≤80、文件 ≤300、位置参数 ≤3、逻辑嵌套 ≤3——widget 构建树可嵌套至 ≤5 层,作为明确例外)。提取独立的
    const
    StatelessWidget
    类,不要使用
    _buildX()
    方法;所有合法位置都使用
    const
    。原因是:小型 const 子树重建更少,也更易阅读。(
    dart3-idioms-and-coding-standards
    、
    widget-composition
    、
    flutter-performance
    )
  11. 名称应体现角色。 使用
    …Screen
    /
    …Notifier
    /
    …Repository
    /
    …Service
    /
    …Failure
    ;严格遵循 Effective Dart 的大小写规范;名称中应包含完整单词和单位;文件应以其中的主要声明命名。原因是:通过 grep 或文件名即可识别所属层级。(
    naming-conventions
    )
  12. 从设计 outset 保障 RTL 与 a11y。 仅使用方向性几何属性(
    start
    /
    end
    ),绝不硬编码 left/right;所有用户字符串均通过
    gen_l10n
    从 ARB 获取;每个语义节点都要设置标签和角色;绝不限制 text scaler,也不能只依赖颜色传达信息。原因是:正确性属性在构建时引入成本很低,事后补救成本却很高。(
    i18n-rtl-l10n
    、
    accessibility-as-code
    )
  13. 测试代码的结构,而不是固定比例。 纯核心使用快速、注入 Clock 的
    package:test
    和属性不变量测试。其余部分使用
    flutter_test
    +
    mocktail
    、
    ProviderContainer.test()
    +
    overrideWith
    ——应 fake repository/services,而不是 fake
    Notifier
    。使用一个 acceptance test 锚定整个应用。原因是:测试应跟随风险分布;纯核心承载关键不变量。(
    testing-strategy
    、
    widget-golden-and-a11y-testing
    )
  14. 严格 lint 是最低标准,格式化不容妥协。 每次 PR 前都必须确保
    dart format
    无差异且
    dart analyze --fatal-infos
    通过;使用在
    pubspec
    中固定版本的
    very_good_analysis
    include,并启用
    strict-casts
    /
    strict-raw-types
    。原因是:分析器通过是最廉价的正确性信号。(
    lint-and-style-config
    、
    dependency-hygiene
    )

Route to the right skill

路由到正确的 skill

When you are…Open
Orienting, unsure which skill governs the task
flutter-conventions-index
(this)
Deciding layers, features-vs-packages, the dependency DAG
flutter-architecture
Scaffolding packages, barrels,
lib/
vs
lib/src/
, workspace layout
project-structure-and-packages
Writing
main()
, error handlers, DI overrides, warm-up, app-lifecycle flush-on-background/resume
app-startup-and-bootstrap
Writing a
Notifier
/
AsyncNotifier
,
family
/
autoDispose
, the single write path
state-management-riverpod
Building or refactoring UI, writing
build()
, layout/insets
widget-composition
Configuring the app router, redirects/auth guards, deep links, nav shells, transitions, PopScope, 404
navigation-and-routing
Building a
Form
, sync/async field validation, focus traversal, keyboard actions, input formatters
forms-and-input
Rendering loading/empty/error states, snackbars, banners, dialogs, Undo, retry
ui-states-and-feedback
Responsive breakpoints, large-screen/tablet/foldable master-detail,
NavigationRail
-vs-
BottomNavigationBar
by width
adaptive-layout
Choosing a Dart 3 construct (sealed, records, class modifiers)
dart3-idioms-and-coding-standards
Naming a class/file/variable/boolean
naming-conventions
Configuring
analysis_options.yaml
, lint severity
lint-and-style-config
Fixing jank, narrowing rebuilds, lazy lists, off-isolate work
flutter-performance
Writing
///
doc comments on the public surface
dartdoc-conventions
Writing async code, guarding
context
/
mounted
, disposal
async-safety
Modeling errors,
Result
/
Failure
, never-lose-data flows
error-handling-typed-results
Writing unit/widget tests, setting up fakes
testing-strategy
Writing golden, layout, RTL, or accessibility widget tests
widget-golden-and-a11y-testing
Adding a Drift table, DAO, or
.watch
stream
persistence-drift
Writing a forward-only schema migration
run-migration
Exporting, backing up, sharing, importing, or restoring user data
data-export-and-restore
Setting up
build_runner
,
build.yaml
, generated-code policy
codegen-and-toolchain
Running codegen before analyze
run-codegen
Writing the GitHub Actions CI pipeline and gates
ci-pipeline-and-gates
Adding ARB strings, ICU plurals, RTL geometry
i18n-rtl-l10n
Authoring
Semantics
, targets, traversal, text scaling
accessibility-as-code
Modeling money, units, dates in a pure core
value-objects-money-and-units
Building on-device reminders/notifications
local-notifications-scheduler
Writing a
CustomPainter
+ gesture hit-testing
custom-canvas-and-gestures
Adding an animation, a haptic, a celebration, or a success/error moment
motion-and-haptics
Generating content from a seed or date every device must reproduce identically
seeded-determinism-and-golden-vectors
Adding ads, a rewarded earn loop, a paywall, or an entitlement gate
ads-and-iap-monetization
Wiring a native channel or platform side effect
service-boundary-and-native
Editing
pubspec
/lockfile, auditing a new dependency
dependency-hygiene
Scaffolding a whole feature module end to end
scaffold-feature-module
Structuring tokens → theme → components (no aesthetic values)
design-system-structure
Re-baselining committed golden images
run-goldens-rebaseline
Running the once-per-app design/QA review before release
design-review-workflow
Cutting a release: versioning, signing, symbols, store declarations, rollout
release-and-store-shipping
当你正在……打开
熟悉项目,或不确定哪个 skill 负责当前任务
flutter-conventions-index
(当前 skill)
决定分层、feature 与 package 的边界、依赖 DAG
flutter-architecture
搭建 package、barrels、
lib/
与
lib/src/
、workspace 布局
project-structure-and-packages
编写
main()
、错误处理器、DI overrides、预热、应用进入后台/恢复时的刷新逻辑
app-startup-and-bootstrap
编写
Notifier
/
AsyncNotifier
、
family
/
autoDispose
、单一写入路径
state-management-riverpod
构建或重构 UI、编写
build()
、处理布局/insets
widget-composition
配置应用 router、重定向/auth guards、deep links、导航壳、transitions、PopScope、404
navigation-and-routing
构建
Form
、同步/异步字段校验、焦点遍历、键盘操作、输入格式化
forms-and-input
渲染加载/空/错误状态、snackbars、banners、dialogs、Undo、retry
ui-states-and-feedback
响应式 breakpoints、大屏/平板/折叠屏 master-detail、根据宽度选择
NavigationRail
或
BottomNavigationBar
adaptive-layout
选择 Dart 3 语法结构(sealed、records、class modifiers)
dart3-idioms-and-coding-standards
为类/文件/变量/布尔值命名
naming-conventions
配置
analysis_options.yaml
、lint severity
lint-and-style-config
解决卡顿、缩小重建范围、延迟加载列表、isolate 外工作
flutter-performance
为公共 API 编写
///
文档注释
dartdoc-conventions
编写异步代码、保护
context
/
mounted
、释放资源
async-safety
建模错误、
Result
/
Failure
、数据不丢失流程
error-handling-typed-results
编写单元/widget 测试、配置 fakes
testing-strategy
编写 golden、布局、RTL 或无障碍 widget 测试
widget-golden-and-a11y-testing
添加 Drift table、DAO 或
.watch
stream
persistence-drift
编写仅向前执行的 schema migration
run-migration
导出、备份、分享、导入或恢复用户数据
data-export-and-restore
配置
build_runner
、
build.yaml
、生成代码策略
codegen-and-toolchain
在 analyze 之前运行 codegen
run-codegen
编写 GitHub Actions CI pipeline 和质量门禁
ci-pipeline-and-gates
添加 ARB 字符串、ICU plurals、RTL 几何布局
i18n-rtl-l10n
编写
Semantics
、targets、traversal、文本缩放
accessibility-as-code
在纯核心中建模金额、单位、日期
value-objects-money-and-units
构建设备端提醒/通知
local-notifications-scheduler
编写
CustomPainter
和手势命中测试
custom-canvas-and-gestures
添加动画、触觉反馈、庆祝效果或成功/错误时刻
motion-and-haptics
根据 seed 或日期生成内容,并确保每台设备都生成完全一致的结果
seeded-determinism-and-golden-vectors
添加广告、rewarded earn loop、paywall 或 entitlement gate
ads-and-iap-monetization
接入 native channel 或平台副作用
service-boundary-and-native
编辑
pubspec
/lockfile、审查新依赖
dependency-hygiene
端到端搭建完整 feature module
scaffold-feature-module
组织 tokens → theme → components(不包含审美类数值)
design-system-structure
为已提交的 golden images 重新建立基线
run-goldens-rebaseline
发布前执行整个应用一次的设计/QA 审查
design-review-workflow
准备发布:版本控制、签名、symbols、商店声明、rollout
release-and-store-shipping

Recommended order when building a feature

构建 feature 时推荐的顺序

  1. Model + pure functions — immutable value types; total, clock-injected core →
    dart3-idioms-and-coding-standards
    ,
    value-objects-money-and-units
  2. Data layer — repository = single source of truth / single write path; Drift table + DAO if persisted →
    persistence-drift
    ,
    error-handling-typed-results
  3. Service seams — every side effect behind an injected interface →
    service-boundary-and-native
  4. ViewModel —
    Notifier
    /
    AsyncNotifier
    ,
    family
    /
    autoDispose
    , intent methods →
    state-management-riverpod
  5. View + widgets — dumb
    ConsumerWidget
    , small
    const
    widget classes, forms, responsive layout, RTL-safe geometry, and the loading/empty/error states before the happy path is called done →
    widget-composition
    ,
    ui-states-and-feedback
    ,
    forms-and-input
    ,
    adaptive-layout
    ,
    i18n-rtl-l10n
    ,
    accessibility-as-code
  6. Wire DI + routing — providers overridden at the composition root; the feature route registers into the single
    go_router
    →
    app-startup-and-bootstrap
    ,
    navigation-and-routing
    ,
    scaffold-feature-module
  7. Docs + tests —
    ///
    on the public surface; core invariants + container/widget tests with fakes →
    dartdoc-conventions
    ,
    testing-strategy
    ,
    widget-golden-and-a11y-testing
  8. Profile, then the CI gate — measure in profile mode; format/analyze/codegen/test green →
    flutter-performance
    ,
    codegen-and-toolchain
    ,
    ci-pipeline-and-gates
  9. Review, then ship — one end-of-build design/QA pass, then the release artifact: versioning, signing, archived symbols, store declarations, staged rollout →
    design-review-workflow
    ,
    release-and-store-shipping
Throughout:
naming-conventions
and
lint-and-style-config
keep every line honest.
  1. 模型 + 纯函数——不可变值类型;全函数、注入 Clock 的核心 →
    dart3-idioms-and-coding-standards
    、
    value-objects-money-and-units
  2. 数据层——repository = 单一事实来源/单一写入路径;需要持久化时使用 Drift table + DAO →
    persistence-drift
    、
    error-handling-typed-results
  3. Service 接缝——每项副作用都置于注入式接口之后 →
    service-boundary-and-native
  4. ViewModel——
    Notifier
    /
    AsyncNotifier
    、
    family
    /
    autoDispose
    、intent methods →
    state-management-riverpod
  5. View + widgets——无逻辑的
    ConsumerWidget
    、小型
    const
    widget 类、表单、响应式布局、RTL-safe 几何布局;在完成 happy path 之前先实现加载/空/错误状态 →
    widget-composition
    、
    ui-states-and-feedback
    、
    forms-and-input
    、
    adaptive-layout
    、
    i18n-rtl-l10n
    、
    accessibility-as-code
  6. 连接 DI + 路由——在组合根覆盖 providers;将 feature route 注册到唯一的
    go_router
    中 →
    app-startup-and-bootstrap
    、
    navigation-and-routing
    、
    scaffold-feature-module
  7. 文档 + 测试——为公共 API 编写
    ///
    ;为核心不变量、container 和 widget 测试配置 fakes →
    dartdoc-conventions
    、
    testing-strategy
    、
    widget-golden-and-a11y-testing
  8. 先分析性能,再设置 CI 门禁——在 profile mode 中测量;确保 format/analyze/codegen/test 全部通过 →
    flutter-performance
    、
    codegen-and-toolchain
    、
    ci-pipeline-and-gates
  9. 先审查,再发布——执行一次构建结束时的设计/QA 审查,然后生成发布产物:版本控制、签名、归档 symbols、商店声明、分阶段 rollout →
    design-review-workflow
    、
    release-and-store-shipping
整个过程中:
naming-conventions
和
lint-and-style-config
会确保每一行代码都经得起检查。

Baseline project setup

基础项目配置

yaml
undefined
yaml
undefined

analysis_options.yaml

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

```bash
include: package:very_good_analysis/analysis_options.yaml # pin the version in pubspec analyzer: language: strict-casts: true strict-raw-types: true

```bash

The PR gate (mirror in CI — see ci-pipeline-and-gates)

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
undefined
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
undefined

The shape every feature repeats

每个 feature 都应复用的结构

dart
// 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>> {
  
  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});
  
  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),
    );
  }
}
dart
// 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>> {
  
  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});
  
  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),
    );
  }
}

Anti-patterns

反模式

  • Data access or business logic inside a widget, a god-
    build()
    , or
    _buildXxx()
    helper methods — untestable and rebuild-unfriendly.
  • setState
    for app/shared state; a mutable model edited by the UI — breaks the single owner + immutable-state rule.
  • get_it
    /
    package:provider
    /legacy
    StateProvider
    /
    StateNotifierProvider
    /
    ChangeNotifierProvider
    — a second DI/state framework to reconcile.
  • A widget or ViewModel writing a DAO directly, or "republish then persist" — bypasses the single write path and can expose half-written state.
  • DateTime.now()
    in domain code — a non-deterministic dependency; inject a
    Clock
    .
  • A swallowed error (
    catch (_) {}
    ), an untyped exception leaking to the UI, or a fire-and-forget
    Future
    — silent failure no lint catches.
  • Hardcoded left/right, a raw digit string, or a user-facing literal in a widget — breaks RTL and localization by construction.
  • !
    /
    late
    /
    dynamic
    to dodge honest types; blanket
    // ignore
    ; fighting
    dart format
    — hides the real defect.
  • Optimizing without profiling, or profiling in debug mode — measures the wrong thing.
  • 在 widget、臃肿的
    build()
    或
    _buildXxx()
    辅助方法中访问数据或编写业务逻辑——难以测试,也不利于重建。
  • 对应用级/共享状态使用
    setState
    ;由 UI 修改可变模型——破坏单一所有者与不可变状态规则。
  • 使用
    get_it
    /
    package:provider
    /旧版
    StateProvider
    /
    StateNotifierProvider
    /
    ChangeNotifierProvider
    ——引入第二套需要协调的 DI/状态框架。
  • Widget 或 ViewModel 直接写入 DAO,或者采用“先重新发布、后持久化”——绕过单一写入路径,并可能暴露只写入一半的状态。
  • 在领域代码中调用
    DateTime.now()
    ——形成不可预测的依赖;应注入
    Clock
    。
  • 吞掉错误(
    catch (_) {}
    )、让未类型化异常泄漏到 UI,或放任
    Future
    自行执行——这些静默失败无法被 lint 发现。
  • 硬编码 left/right、使用原始数字字符串,或在 widget 中使用面向用户的字面量——破坏 RTL 与本地化保障。
  • 使用
    !
    /
    late
    /
    dynamic
    逃避真实类型检查;使用全局性的
    // ignore
    ;与
    dart format
    对抗——这些做法会掩盖真实缺陷。
  • 未进行 profiling 就优化,或在 debug mode 中 profiling——得到的测量结果不具代表性。

Definition of done

完成定义

  • Code sits in the correct feature/layer; the dependency graph only points downward (rules 1, 6).
  • The View is dumb; one
    Notifier
    /
    AsyncNotifier
    owns immutable state; no legacy provider or extra DI container (rules 2–4).
  • Every mutation goes through a repository's single write path — persist, then republish (rule 5).
  • Models immutable with value equality; pure functions total; recoverable I/O is
    Result
    + sealed
    Failure
    , exhaustively switched; no swallowed errors (rule 7).
  • Side effects are behind injected interfaces; "now" comes from a
    Clock
    (rule 8).
  • Async is awaited/handled;
    context
    /
    mounted
    guarded after awaits; controllers and subscriptions disposed (rule 9).
  • Units within limits; widget classes extracted;
    const
    applied; names carry role + units (rules 10–11).
  • RTL-safe geometry; strings in ARB; semantics labeled; text scaler not clamped (rule 12).
  • Public surface has
    ///
    docs; core invariants + container/widget tests with fakes pass (rule 13).
  • dart format
    +
    dart analyze --fatal-infos
    clean on a pinned lint include (rule 14).
  • The right specialized skill was consulted for the depth of the task.
  • 代码位于正确的 feature/层级;依赖图只向下指向(规则 1、6)。
  • View 不包含业务逻辑;一个
    Notifier
    /
    AsyncNotifier
    持有不可变状态;不使用旧版 provider 或额外 DI 容器(规则 2–4)。
  • 每次变更都经过 repository 的单一写入路径——先持久化,再重新发布(规则 5)。
  • 模型不可变并具有值相等性;纯函数为全函数;可恢复 I/O 使用
    Result
    + sealed
    Failure
    并进行穷尽处理;不吞掉错误(规则 7)。
  • 副作用位于注入式接口之后;“当前时间”来自
    Clock
    (规则 8)。
  • 异步操作均被 await 或显式处理;await 之后保护
    context
    /
    mounted
    ;释放 controllers 和 subscriptions(规则 9)。
  • 单元大小符合限制;提取独立的 widget 类;应用
    const
    ;名称体现角色和单位(规则 10–11)。
  • 几何布局支持 RTL;字符串来自 ARB;语义节点具有标签;不限制 text scaler(规则 12)。
  • 公共 API 包含
    ///
    文档;使用 fakes 的核心不变量、container 和 widget 测试全部通过(规则 13)。
  • 在固定版本的 lint include 下,
    dart format
    +
    dart analyze --fatal-infos
    无问题(规则 14)。
  • 已针对任务深度查阅正确的专业 skill。

Related skills

相关 skills

Every skill in this table is a sibling; open the one the routing table points to. Start with
flutter-architecture
for where code goes,
state-management-riverpod
for how state flows, and
error-handling-typed-results
for the
Result
/
Failure
spine.
表中的每个 skill 都与当前 skill 同级;请打开路由表所指向的 skill。先通过
flutter-architecture
了解代码应放在哪里,通过
state-management-riverpod
了解状态如何流动,再通过
error-handling-typed-results
了解
Result
/
Failure
主干。

References

参考资料