widget-composition
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWidget Composition
Widget 组合
Build every screen from many small, , single-purpose widget classes. A widget that does one thing rebuilds cheaply, reads clearly, tests in isolation, and can be lifted into a shared component library. The View is dumb: it watches one ViewModel and renders; logic lives in the Notifier, state in immutable value objects, layout in directional structural primitives. Applies to any screen, list, card, or shared component.
constRead the reference for the task at hand:
- — why a class beats a method (
references/rebuild-mechanics.md),Element.updateChildcanonicalization, the full key policy, andconstfor narrowed rebuilds..select - — edge-to-edge background vs
references/structural-layout.mdcontent, computed cell sizing, theSafeAreacross/main-axis trap, directional insets, and IME/GridView.resizeToAvoidBottomInset - — cheapest-widget table, data-class decision table (drift row vs record vs hand-written vs freezed), and gesture→fallback rules.
references/widget-and-data-selection.md
Run before a PR.
scripts/check-widget-composition.sh每个 screen 都应由许多小型、、单一职责的 Widget 类组成。只做一件事的 widget rebuild 成本低、读起来清晰、可独立测试,并且可以提取到共享组件库中。View 应当保持 dumb:只 watch 一个 ViewModel 并负责渲染;逻辑放在 Notifier 中,状态放在不可变的 value object 中,布局则使用支持方向的结构化 primitive。适用于任何 screen、列表、卡片或共享组件。
const根据当前任务阅读对应的参考资料:
- ——为什么 class 优于 method(
references/rebuild-mechanics.md)、Element.updateChild规范化、完整的 key 策略,以及通过const缩小 rebuild 范围。.select - ——edge-to-edge 背景与
references/structural-layout.md内容、动态计算的 cell 尺寸、SafeArea交叉轴/主轴陷阱、方向性 inset,以及 IME/GridView。resizeToAvoidBottomInset - ——开销最低的 widget 对照表、data class 决策表(drift row、record、手写类或 freezed),以及手势到回退操作的规则。
references/widget-and-data-selection.md
提交 PR 前运行 。
scripts/check-widget-composition.shNon-negotiable rules
不可妥协的规则
- Extract a class, never a
Widget-returning method.Widgetis not a widget — its subtree has noWidget _buildHeader()of its own, soElementnever gets to compare old vs. new and short-circuit; it rebuilds with the parent, cannot beElement.updateChild, cannot take aconst, andKeycannot reach it in a test.find.byType - everywhere it is legal, every field
const.finalconstructors canonicalize identical widgets to the same object, letting Flutter prune the subtree rebuild. Turn onconst+prefer_const_constructors. Budget zero minutes hand-tuning const — letprefer_const_constructors_in_immutableswrite it; never restructure a tree to make something const-able.dart fix --apply - Keep small and shallow. Past the complexity limits owned by
build()(dart3-idioms-and-coding-standards≤ 80 lines; widget build nesting ≤ 5 — the deeper-nesting exception that table states for widget trees vs. logic ≤ 3), extract child widget classes. Abuild()you must scroll to read is too big.build() - One responsibility per widget. A component renders one thing; it does not also fetch data, format values, read a clock, hold app state, or schedule side effects.
- No I/O, formatting, or domain math in .
build()can run 60×/second — no repository/DAO call, nobuild(), noDateTime.now(), no sorting. The Notifier precomputes a ready-to-render UI-state value; the widget only paints it.NumberFormat - The View is dumb and watches exactly one ViewModel. A does
ConsumerWidgetand renders; intents go back viaref.watch(oneNotifierProvider). It never reaches a repository or DAO directly and never mutates persisted state.ref.read(...notifier) - /
StatelessWidgetby default. UseConsumerWidget/StatefulWidgetonly for genuinely local, ephemeral UI state (ConsumerStatefulWidget,AnimationController,TextEditingController,ScrollController). App/domain/session state lives in the Notifier.FocusNode - Always controllers created in
dispose(). LeakedState/AnimationController/TextEditingController/ScrollController/subscriptions retain memory and fire after teardown.FocusNode/cancel_subscriptionsare errors.close_sinks - Lists are lazy. Use /
ListView.builderfor variable/long lists. NeverSliverListfor unbounded data — it builds every row eagerly.ListView(children: items.map(...).toList()) - Resolve identity, not content, in callbacks. A tap closure captures the stable id (), never a captured content value — a re-tap after an edit must not act on a stale capture.
onTap: () => onSelect(item.id) - Every gesture needs a visible, focusable fallback. Any behaviour reachable by touch must also be reachable by a labelled focusable control. A drag-only or long-press-only action is unreachable to switch-access and screen-reader users.
- Style from theme/tokens and logical directions. Colors/text styles come from or a
Theme.of(context), never literal hex. UseThemeExtension/EdgeInsetsDirectional(AlignmentDirectional/start), never hardcoded left/right, so RTL mirrors by construction.end
- 提取 类,绝不要使用返回
Widget的方法。Widget并不是一个独立 widget——它的子树没有自己的Widget _buildHeader(),因此Element无法比较新旧实例并提前短路;它会随父级一起 rebuild,无法使用Element.updateChild,不能接收const,测试中也无法通过Key找到它。find.byType - 在合法处一律使用 ,每个字段都设为
const。finalconstructor 会将相同的 widget 规范化为同一对象,让 Flutter 可以跳过子树重建。启用const和prefer_const_constructors。不要浪费时间手工调优 const——让prefer_const_constructors_in_immutables来写;也不要为了使某段内容可声明为 const 而重构 widget 树。dart fix --apply - 保持 小而浅。 在
build()所定义的复杂度限制内(dart3-idioms-and-coding-standards≤ 80 行;widget build 嵌套 ≤ 5——相对于逻辑代码 ≤ 3,该表为 widget 树规定了更深嵌套的例外),应提取子 widget 类。需要滚动才能读完的build()一定太大。build() - 每个 widget 只承担一项职责。 组件只负责渲染一件事;它不应同时获取数据、格式化数值、读取时钟、持有应用状态或调度副作用。
- 禁止在 中执行 I/O、格式化或领域计算。
build()每秒最多可能执行 60 次——不得调用 repository/DAO、调用build()、使用DateTime.now()或执行排序。Notifier 应预先计算出可直接渲染的 UI-state 值;widget 只负责绘制它。NumberFormat - View 保持 dumb,并且只 watch 恰好一个 ViewModel。 执行
ConsumerWidget并负责渲染;intent 通过ref.watch(oneNotifierProvider)回传。它绝不直接访问 repository 或 DAO,也绝不修改持久化状态。ref.read(...notifier) - 默认使用 /
StatelessWidget。 只有真正属于本地的临时 UI 状态才使用ConsumerWidget/StatefulWidget(如ConsumerStatefulWidget、AnimationController、TextEditingController、ScrollController)。应用状态、领域状态和会话状态都放在 Notifier 中。FocusNode - 始终 在
dispose()中创建的 controller。 泄漏的State/AnimationController/TextEditingController/ScrollController/subscription 会保留内存,并在 teardown 后继续触发。遗漏FocusNode/cancel_subscriptions属于错误。close_sinks - 列表必须惰性构建。 对可变或长列表使用 /
ListView.builder。对于无界数据,绝不要使用SliverList,因为它会立即构建所有 row。ListView(children: items.map(...).toList()) - 回调中解析身份标识,而不是内容。 点击闭包应捕获稳定的 id(),绝不捕获内容值——编辑后再次点击时,不得对过期捕获执行操作。
onTap: () => onSelect(item.id) - 每个手势都需要可见且可聚焦的回退操作。 任何可通过触摸触达的行为,都必须能够通过带标签且可聚焦的控件触达。仅支持拖拽或长按的操作,对 switch-access 用户和屏幕阅读器用户不可用。
- 使用 theme/token 和逻辑方向设置样式。 颜色和文本样式来自 或
Theme.of(context),绝不使用字面量 hex。使用ThemeExtension/EdgeInsetsDirectional(AlignmentDirectional/start),绝不硬编码 left/right,从而确保 RTL 自动镜像。end
Extract a widget vs. a method
提取 widget 还是使用 method
dart
// WRONG — helper method: rebuilds with parent, no const, no key, no isolation, untestable
Widget _buildProductCard(Product product) {
return Card(child: Text(product.name));
}
// RIGHT — widget class: const-constructible, keyable, independently rebuildable, testable
class ProductCard extends StatelessWidget {
const ProductCard({required this.name, required this.price, this.onTap, super.key});
final String name; // already-localized; the feature mapped Product -> name
final String price; // already-formatted; no currency math here
final VoidCallback? onTap;
Widget build(BuildContext context) {
final theme = Theme.of(context); // read theme in the leaf that uses it
return Card(
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsetsDirectional.all(12), // logical, RTL-safe
child: Row(
children: [
Expanded(child: Text(name, style: theme.textTheme.titleMedium)),
Text(price, style: theme.textTheme.bodyMedium),
],
),
),
),
);
}
}dart
// WRONG — helper method: rebuilds with parent, no const, no key, no isolation, untestable
Widget _buildProductCard(Product product) {
return Card(child: Text(product.name));
}
// RIGHT — widget class: const-constructible, keyable, independently rebuildable, testable
class ProductCard extends StatelessWidget {
const ProductCard({required this.name, required this.price, this.onTap, super.key});
final String name; // already-localized; the feature mapped Product -> name
final String price; // already-formatted; no currency math here
final VoidCallback? onTap;
Widget build(BuildContext context) {
final theme = Theme.of(context); // read theme in the leaf that uses it
return Card(
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsetsDirectional.all(12), // logical, RTL-safe
child: Row(
children: [
Expanded(child: Text(name, style: theme.textTheme.titleMedium)),
Text(price, style: theme.textTheme.bodyMedium),
],
),
),
),
);
}
}Keep build() lean — the Notifier precomputes
保持 build() 精简——由 Notifier 预计算
dart
// WRONG — domain math + formatting inside build()
Widget build(BuildContext context, WidgetRef ref) {
final order = ref.read(orderRepositoryProvider).find(id); // repo call in build!
final remaining = order.total - order.paid; // domain math in UI
final label = NumberFormat.currency().format(remaining); // formatting in UI
return Text(remaining <= 0 ? 'Done' : label);
}
// RIGHT — the Notifier exposes ready-to-render UI state; select narrows the rebuild
Widget build(BuildContext context, WidgetRef ref) {
final status = ref.watch(orderNotifierProvider(id).select((s) => s.balanceStatus));
return Text(status.label, style: status.isSettled ? _settledStyle : _dueStyle);
}ref.watch(p.select((s) => s.field))conststate-management-riverpodflutter-performance.selectdart
// WRONG — domain math + formatting inside build()
Widget build(BuildContext context, WidgetRef ref) {
final order = ref.read(orderRepositoryProvider).find(id); // repo call in build!
final remaining = order.total - order.paid; // domain math in UI
final label = NumberFormat.currency().format(remaining); // formatting in UI
return Text(remaining <= 0 ? 'Done' : label);
}
// RIGHT — the Notifier exposes ready-to-render UI state; select narrows the rebuild
Widget build(BuildContext context, WidgetRef ref) {
final status = ref.watch(orderNotifierProvider(id).select((s) => s.balanceStatus));
return Text(status.label, style: status.isSettled ? _settledStyle : _dueStyle);
}ref.watch(p.select((s) => s.field))conststate-management-riverpodflutter-performance.selectComposition guidance
组合指南
- Split a screen into sections (header, body, footer) as separate widget classes; split a row into its parts when a part is reused or complex.
- Pass data down, callbacks up. A component receives what it shows and exposes callbacks; it never reaches into a parent or a provider it does not own.
onX - Read /
MediaQueryin the leaf that needs it, not at the top of a bigTheme— reading at the top rebuilds the whole subtree on every metrics change.build() - Prefer the cheapest widget: over
SizedBoxfor spacing;Container/ColoredBoxoverDecoratedBoxfor a single effect;Container/AlignoverCenter+Row. SeeSpacer.references/widget-and-data-selection.md - Hoist unchanging subtrees into locals so they survive rebuilds.
const - A shared component is domain-blind and provider-free. It takes primitives, localized labels, immutable view-data, and callbacks through its constructor — never a domain entity, a repository, or a . The dependency arrow is one-way: features import the shared library, never the reverse.
ref
- 将 screen 拆分为多个 section(header、body、footer),每个 section 使用独立的 widget class;如果 row 中的某一部分会被复用或逻辑复杂,也应继续拆分。
- 数据向下传递,回调向上传递。 组件只接收展示所需的数据,并通过 回调对外暴露;绝不访问父级,也不读取自己不拥有的 provider。
onX - 只在需要它的叶子节点中读取 /
MediaQuery,不要在大型Theme顶部读取——在顶部读取会导致每次尺寸变化都重建整个子树。build() - 优先使用开销最低的 widget: 间距使用 而非
SizedBox;单一视觉效果使用Container/ColoredBox而非DecoratedBox;居中或对齐使用Container/Align,而不是Center+Row。参见Spacer。references/widget-and-data-selection.md - 将不会变化的子树提升为 局部变量,使其能够在 rebuild 后继续复用。
const - 共享组件应与领域无关且不依赖 provider。 它只通过 constructor 接收基础类型、已本地化标签、不可变 view-data 和回调——绝不接收领域实体、repository 或 。依赖方向是单向的:feature 导入 shared library,反向依赖绝不允许。
ref
Keys
Key
Keys matter in exactly one situation: the framework must decide whether an (and its ) can be reused for a new at the same position in a child list. The classic bug is stale bleeding into a reused slot after a list reorders.
ElementStateWidgetState| Situation | Key |
|---|---|
| A row in a reorderable / filterable / variable list | |
| A fixed-order, fixed-length list; leaf stateless widgets | none — a key changes nothing |
| Swapping two widgets of the same type and needing to preserve state | a |
A widget that suddenly "wants" a | stop — that is a design smell, not a fix |
Never add for identity or lookups — it forces a global registry lookup and enables cross-tree reparenting, machinery most apps do not need. Full mechanism in .
GlobalKeyreferences/rebuild-mechanics.mdKey 仅在一种情况下很重要:框架需要判断同一子列表位置上的新 Widget 是否可以复用旧 Widget 的 (及其 )。典型 bug 是列表重排后,过期的 渗入被复用的 slot。
ElementStateState| 场景 | Key |
|---|---|
| 可重排、可过滤或长度可变列表中的 row | |
| 顺序固定、长度固定的列表;叶子 stateless widget | 无——key 不会带来任何变化 |
| 交换两个同类型 widget,且需要保留其状态 | 使用能够区分二者的 |
突然“想要”使用 | 停止——这是设计异味,而不是修复方案 |
绝不要为了身份标识或查找而添加 ——它会强制执行全局 registry 查找,并支持跨树 reparenting,大多数应用都不需要这些机制。完整机制见 。
GlobalKeyreferences/rebuild-mechanics.mdData classes for view-data
用于 view-data 的 data class
Do not hand-roll a data class where a generator already emits one. The decision rule:
| Shape | Use |
|---|---|
| Persisted row | the drift-generated row class directly — it has |
| Ephemeral multi-return inside one layer | a record — |
| Hand-authored domain value / UI-state | |
| A joined shape no generator emits | hand-written |
Do not add for one ; is five lines. Full table and rationale in . See for value-type and record rules.
equatable==Object.hash(...)references/widget-and-data-selection.mddart3-idioms-and-coding-standards如果已有 generator 能生成 data class,就不要手写。决策规则如下:
| 结构 | 使用方式 |
|---|---|
| 持久化 row | 直接使用由 drift 生成的 row class——它已经具备 |
| 某一层内部的临时多返回值 | 使用 record—— |
| 手写的领域值或 UI-state | 简单值使用 |
| 没有 generator 可生成的组合结构 | 手写 |
不要仅为实现一个 就添加 ; 只需五行。完整对照表和设计理由见 。value type 和 record 规则参见 。
==equatableObject.hash(...)references/widget-and-data-selection.mddart3-idioms-and-coding-standardsStructural layout
结构化布局
The window is edge-to-edge; the targets are inset — never the same question. Paint the background full-bleed behind the system bars, then wrap the interactive content in and add margins inside it.
SafeAreadart
Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface, // full-bleed, behind bars
resizeToAvoidBottomInset: false, // a fixed grid must not reflow under the IME
body: SafeArea(
child: Padding(
padding: const EdgeInsetsDirectional.all(16), // logical inset, RTL-safe
child: child,
),
),
)Compute cell size from the viewport; never hardcode a tile dimension and never assert a minimum one (a size floor fires exactly where large text needs the room most — the real constraint is that the label fits). The axis trap is silent: in a vertical grid, is the column gap and is the row gap.
GridViewcrossAxisSpacingmainAxisSpacingdart
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12, // COLUMN gap in a vertical grid
mainAxisSpacing: 16, // ROW gap
),
itemCount: items.length,
itemBuilder: (context, i) => ProductCard(name: items[i].name, price: items[i].price),
);Full treatment — computed sizing math, IME behaviour, , and when to use a +grid instead of a spanning cell — in . See for reading a11y state from and never clamping , and for token/theme structure.
MediaQuery.viewInsetsColumnreferences/structural-layout.mdaccessibility-as-codeMediaQuerytextScalerdesign-system-structure窗口采用 edge-to-edge;交互目标则需要 inset——不要把二者当成同一个问题。背景应在 system bar 后方满幅绘制,而交互内容应包裹在 中,并在其内部添加 margin。
SafeAreadart
Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface, // full-bleed, behind bars
resizeToAvoidBottomInset: false, // a fixed grid must not reflow under the IME
body: SafeArea(
child: Padding(
padding: const EdgeInsetsDirectional.all(16), // logical inset, RTL-safe
child: child,
),
),
)根据 viewport 计算 cell 尺寸;绝不硬编码 tile 尺寸,也绝不设置尺寸下限——尺寸下限恰好会在大文本最需要空间时触发,真正的约束是 label 必须能够容纳。 的轴向陷阱悄无声息:在纵向 grid 中, 是列间距, 是行间距。
GridViewcrossAxisSpacingmainAxisSpacingdart
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 12, // COLUMN gap in a vertical grid
mainAxisSpacing: 16, // ROW gap
),
itemCount: items.length,
itemBuilder: (context, i) => ProductCard(name: items[i].name, price: items[i].price),
);完整处理方式——包括动态尺寸计算、IME 行为、,以及何时应使用 + grid,而不是让某个单元格横跨多列——见 。从 读取 a11y 状态以及绝不限制 ,参见 ;token/theme 结构参见 。
MediaQuery.viewInsetsColumnreferences/structural-layout.mdMediaQuerytextScaleraccessibility-as-codedesign-system-structureAnti-patterns
反模式
- helper methods for any non-trivial UI — no
Widget _buildXxx()boundary, no const, no key, untestable. Make a class.Element - God-widget: a 300-line with the whole screen inline.
build() - Repository/DAO/call in
SharedPreferencesorbuild()— route through the Notifier and a single write path.initState() - in a widget — a widget never reads a wall clock; the Notifier precomputes time-derived values from
DateTime.now()injected viaClock(seeclockProvider).state-management-riverpod - Creating controllers in (recreated every frame) instead of in
build()+State.dispose() - to refresh app/session data that belongs in a Notifier —
setState()is only for local ephemeral UI.setState - — builds every row eagerly. Use
ListView(children: hugeList.map(...).toList())..builder - for everything (over-wrapping) — use the specific, cheaper widget.
Container - /
GlobalKeyfor identity — delete; useObjectKeywhere a list reorders.ValueKey(id) - Hardcoded hex / left-right padding / literal user strings — use theme tokens, , and ARB localization.
EdgeInsetsDirectional - A gesture with no visible fallback, or clamping to force a layout to fit — fix the layout, not the scale.
textScaler - A shared-library widget importing a domain type or reading a provider — it can no longer be reasoned about or tested in isolation.
- 对任何非简单 UI 使用 辅助方法——它没有
Widget _buildXxx()边界、没有 const、没有 key,也无法独立测试。应当创建 class。Element - God-widget:将整个 screen 内联到 300 行的 中。
build() - 在 或
build()中调用 Repository/DAO/initState()——应通过 Notifier 和单一写入路径处理。SharedPreferences - 在 widget 中调用 ——widget 绝不读取墙上时钟;Notifier 应从通过
DateTime.now()注入的clockProvider预计算时间派生值(参见Clock)。state-management-riverpod - 在 中创建 controller(每帧都会重新创建),而不是在
build()中创建并调用State。dispose() - 使用 刷新属于 Notifier 的应用或会话数据——
setState()仅用于本地临时状态。setState - 使用 ——会立即构建所有 row。改用
ListView(children: hugeList.map(...).toList())。.builder - 所有地方都使用 ,造成过度包装——应使用更具体、开销更低的 widget。
Container - 使用 /
GlobalKey作为身份标识——应删除;可重排列表使用ObjectKey。ValueKey(id) - 硬编码 hex、left-right padding 或直接写入用户文案——应使用 theme token、和 ARB localization。
EdgeInsetsDirectional - 手势没有可见回退操作,或为了迫使布局适配而限制 ——应修复布局,而不是缩放比例。
textScaler - 共享库 widget 导入领域类型或读取 provider——这样将无法再对其进行独立推理和测试。
Definition of done
完成标准
- Every reusable/non-trivial subtree is a class (not a
Widgetmethod);_buildX()where legal; all fieldsconst.final - ≤ ~80 lines; nesting shallow; complex parts extracted.
build() - No I/O, repository call, , formatting, or domain math in
DateTime.now()— the Notifier precomputes UI state.build() - The View is a watching one Notifier; intents routed via
ConsumerWidget; nothing mutates persisted state in the widget.ref.read(...notifier) - only for local ephemeral state; every created controller
StatefulWidgetd.dispose() - Long/variable lists use ; reorderable lists carry
.builder; noValueKey(id)for identity.GlobalKey - Callbacks resolve a stable id, not a captured content value.
- Any shared widget takes primitives/callbacks only, imports no domain type and no provider.
- Background full-bleed, content in ; directional insets;
SafeAreaspacing axes correct; no hardcoded/floored cell size.GridView - Styling from theme/tokens; every gesture has a visible focusable fallback; never clamped.
textScaler - clean;
dart formatclean;dart analyze --fatal-infospasses.scripts/check-widget-composition.sh
- 每个可复用或非简单子树都是 class,而不是
Widget方法;合法处使用_buildX();所有字段均为const。final - ≤ 约 80 行;嵌套浅显;复杂部分已提取。
build() - 中没有 I/O、repository 调用、
build()、格式化或领域计算——由 Notifier 预计算 UI state。DateTime.now() - View 是 watch 一个 Notifier 的 ;intent 通过
ConsumerWidget路由;widget 不修改任何持久化状态。ref.read(...notifier) - 仅用于本地临时状态;所有创建的 controller 均已
StatefulWidget。dispose() - 长列表或可变列表使用 ;可重排列表携带
.builder;不以ValueKey(id)作为身份标识。GlobalKey - 回调解析稳定的 id,而不是捕获内容值。
- 所有共享 widget 只接收基础类型和回调,不导入领域类型,也不读取 provider。
- 背景满幅铺开,内容位于 中;使用方向性 inset;
SafeArea间距轴向正确;cell 尺寸既不硬编码也不设置下限。GridView - 样式来自 theme/token;每个手势都有可见且可聚焦的回退操作;绝不限制 。
textScaler - 无差异;
dart format无问题;dart analyze --fatal-infos检查通过。scripts/check-widget-composition.sh
Related skills
相关 skill
- — the layer DAG and the dumb-View contract this widget sits inside; widget-composition is the deep dive on building those Views.
flutter-architecture - — const subtrees,
flutter-performancerebuild scoping,.select, dispose discipline, measuring in profile mode.RepaintBoundary - — the one-Notifier-per-feature ViewModel this View watches, and the watch/read/listen split.
state-management-riverpod - — how tokens/theme/components are structured and consumed (the source of the styles this skill reads).
design-system-structure - — reading a11y state from
accessibility-as-code,MediaQueryroles/labels, sort keys, never clampingSemantics.textScaler - — owns the directional-geometry (
i18n-rtl-l10n/EdgeInsetsDirectional) and ARB-localization rules the strings and insets these widgets render must follow.AlignmentDirectional - — the complexity-limit table these rules cite; which construct each data class earns; records intra-layer only; immutable value types.
dart3-idioms-and-coding-standards - — responsive breakpoints, tablet/foldable master-detail, and NavigationRail-vs-BottomNav by width (the responsiveness layer around these structural primitives).
adaptive-layout - —
forms-and-input/Formvalidation, focus traversal, and keyboard-action wiring for the input fields these screens compose.TextFormField - — pumping an extracted widget class and asserting layout/overflow/golden across device and scale.
widget-golden-and-a11y-testing - — the one exception where pixels live on a
custom-canvas-and-gesturesinstead of a widget tree.CustomPainter
- ——widget 所处的层级 DAG 和 dumb-View 契约;widget-composition 则是深入构建这些 View 的专题说明。
flutter-architecture - ——const 子树、
flutter-performancerebuild 范围控制、.select、dispose 纪律,以及在 profile mode 中进行测量。RepaintBoundary - ——每个 feature 使用一个 Notifier 的 ViewModel,以及 watch/read/listen 的职责划分。
state-management-riverpod - ——token/theme/component 的组织与使用方式,也是此 skill 所读取样式的来源。
design-system-structure - ——从
accessibility-as-code读取 a11y 状态、MediaQueryrole/label、sort key,以及绝不限制Semantics。textScaler - ——负责方向几何(
i18n-rtl-l10n/EdgeInsetsDirectional),以及 widget 所渲染字符串和 inset 必须遵循的 ARB-localization 规则。AlignmentDirectional - ——这些规则引用的复杂度限制表;每种数据结构应采用的构造;record 仅限层内使用;不可变 value type。
dart3-idioms-and-coding-standards - ——响应式断点、tablet/foldable master-detail,以及根据宽度在 NavigationRail 和 BottomNav 之间选择。
adaptive-layout - ——
forms-and-input/Form校验、focus traversal,以及输入字段的 keyboard-action 连接。TextFormField - ——泵入提取后的 widget class,并跨设备和缩放比例断言布局、overflow 与 golden。
widget-golden-and-a11y-testing - ——唯一允许像素绘制在
custom-canvas-and-gestures中,而非 widget 树中的例外。CustomPainter
References
参考资料
- Flutter — Performance best practices (const constructors, minimize rebuilds, lazy builders)
- Flutter — UI layer case study / dumb views
- Flutter API — ·
StatelessWidget·ListView.builder·EdgeInsetsDirectionalSafeArea - Riverpod — Reading a provider / ,
ConsumerWidgetselect - Dart — Effective Dart: Style · Linter rules
- Flutter——性能最佳实践(const constructor、减少 rebuild、惰性 builder)
- Flutter——UI 层案例研究 / dumb View
- Flutter API——·
StatelessWidget·ListView.builder·EdgeInsetsDirectionalSafeArea - Riverpod——读取 provider / 、
ConsumerWidgetselect - Dart——Effective Dart:Style · Linter 规则