要点
Flutter 入门阶段的名词按用途分成七组:Widget 基础、布局、滚动、状态、导航、调试、报错。每个条目给签名、一句话定位、最小示例和坑。字典是查的不是读的——遇到具体问题从分组标题跳进去,文末速查表做全量索引。
Widget 基础
组内共性:Widget 分无状态(StatelessWidget,纯配置)与有状态(StatefulWidget,数据跨帧保留)两种。生命周期说的是 State 对象的生命周期,不是 Widget 的——Widget 随时可能被重建,State 在树结构不变期间被保留复用。
StatelessWidget
class TitleText extends StatelessWidget {
const TitleText(this.text, {super.key});
final String text;
@override
Widget build(BuildContext context) => Text(text,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold));
}
build 输出完全由构造参数决定时用它;框架可以随时丢弃重建它,不替你保存任何东西。
⚠️ 坑:在 StatelessWidget 里持有可变字段(非 final)并指望界面跟着变——没有任何机制通知它重建,改了也是白改。
StatefulWidget 与 State
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
var count = 0;
@override
Widget build(BuildContext context) => TextButton(
onPressed: () => setState(() => count++), // 改数据必须包进 setState
child: Text('点了 $count 次'));
}
数据要跨帧保留(计数、输入内容、动画进度)时用它;状态放 State 字段里,setState 标记后下一帧重跑 build。
⚠️ 坑:直接写 count++ 不包 setState,数据变了界面不动;这是新手第一大”为什么没反应”。
State 生命周期
顺序:initState(一次性初始化、订阅、controller 创建)→ didChangeDependencies(依赖的 InheritedWidget 变化时再触发)→ build(一帧内可能多次)→ didUpdateWidget(父重建传入新配置)→ dispose(取消订阅、释放 controller)。
⚠️ 坑:把网络请求、订阅写进 build——build 会被反复调用,副作用会成倍执行;初始化只放 initState,清理只放 dispose。忘了 dispose controller 是内存泄漏的标准来源(Dart 的 GC 不会替你调 dispose)。
布局
组内共性:布局分单子(Container、Stack)与多子(Row、Column)。底层心智是约束传递(constraints go down, sizes go up):父给子约束,子在约束内选尺寸,父决定位置。多数溢出报错是约束没谈拢,不是像素算错。
Container
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Text('卡片'),
)
装饰盒子的快捷组合:padding、margin、边框、背景、圆角一体;什么都不传时它基本什么都不做。
⚠️ 坑:width/height 与 constraints、alignment 同时给会互相打架,最终尺寸以 constraints 优先;只用单一装饰时直接用 Padding/ColoredBox 更省一层。
Row / Column
Column(
mainAxisAlignment: MainAxisAlignment.center, // 主轴对齐
crossAxisAlignment: CrossAxisAlignment.start, // 交叉轴对齐
children: const [Text('第一行'), Text('第二行')],
)
主轴水平(Row)/垂直(Column)排列子项;两个方向参数名字相同,行为随主轴切换。
⚠️ 坑:Row 里再嵌 Row、或 ListView 里直接放 Column 高度未定,会得到 unbounded(无界约束)类报错;内容超出可用宽度时就是著名的黄黑条纹溢出。
Stack
Stack(
children: [
Image.network(url), // 非定位子项决定 Stack 尺寸
const Positioned(right: 8, bottom: 8, child: Icon(Icons.edit)),
],
)
子项叠放;非定位子项按 alignment 摆,Positioned 子项按四边偏移定位。
⚠️ 坑:Positioned 子项不参与 Stack 的尺寸计算——只放 Positioned 子项的 Stack 尺寸为零;角标场景记得底下有个”撑尺寸”的子项。
Expanded / Flexible
Row(children: [
const Expanded(child: Text('标题很长的')), // tight:强制占满剩余空间
Flexible(child: IconButton(onPressed: null, icon: const Icon(Icons.share))),
])
在 Row/Column 主轴上分配剩余空间。Expanded 等于 Flexible(fit: FlexFit.tight)——强制填满;默认 loose 允许子项按自己意愿更小。
⚠️ 坑:多个 Expanded 默认 flex 均 1 平分空间,比例用 flex 参数调;Expanded 内子项拿到的是无限主轴约束,里面再放需要固有尺寸的东西(如没约束的 ListView)会报无界错误。
滚动
组内共性:滚动内容交给 ListView 家族或更低层的 sliver。两个高频疑问——”Column 超屏为什么不能滚”(Column 不是滚动组件)与”列表为什么卡”(构建策略不对)都能在本组找到答案。
ListView(children: …)
ListView(
children: items.map((e) => ListTile(title: Text(e))).toList(),
)
饿汉式:children 全量立刻构建。适合十条八条的固定内容。
⚠️ 坑:上百条或每条构建成本高(图片、复杂布局)时,首帧把所有子项都建出来,滚动掉帧、内存上升——这是构建策略问题,换手机解决不了。
ListView.builder
ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) => ListTile(
title: Text(items[i]),
// 列表项含输入框等有状态内容时给 key:滚动复用时不串状态
key: ValueKey(items[i].id),
),
)
惰性构建:itemBuilder 只在可见范围与缓存区被调用,itemCount 必须给准。性能语义上与 children 全量构建是两个世界,长列表的默认答案。
⚠️ 坑:itemCount 报多了越界崩、报少了尾部静默丢失;在 itemBuilder 里做订阅或创建 controller——itemBuilder 会随滚动反复调用,副作用会翻倍。
状态
组内共性:按”谁读得到”分层——组件内部用 setState,子树共享用 InheritedWidget,跨页面/全局再上 Provider 或 Riverpod。选型原则是先用小的,痛点出现了再上大的。
setState
一句话定位:组件内部状态的最小方案,标记当前 State 为脏、下一帧重跑 build。上面的计数器就是完整用法,没有更多秘密。
⚠️ 坑:dispose 之后调用(异步回调回来页面已销毁)与 build 过程中调用都会直接报错,见文末报错速查;mounted 检查是标准防御。
InheritedWidget
// 主题、Locale、屏幕信息都是它下发的;自己写的话样板极多
Theme.of(context) // 内部就是沿树查找 InheritedWidget 并建立依赖
MediaQuery.of(context)
一句话定位:沿树高效下发的只读共享数据,子树 O(1) 查找,依赖它的子树在其变化时自动重建。
⚠️ 坑:它只负责”分发与通知”,不负责”更新触发”;直接拿它当全局状态用,样板代码量会劝退你——这正是 Provider 存在的原因。
Provider / Riverpod
一句话定位:Provider 在 InheritedWidget 上封装了依赖注入,是”局部状态升级到共享状态”的中间站;Riverpod 把状态做成可组合、可测试、不依赖 Widget 树的 Provider 网络,是当前社区主流的大状态方案。安装均不钉版本:flutter pub add provider 或 flutter pub add flutter_riverpod。
⚠️ 坑:把一切塞进全局——能 setState 解决的局部状态不要提升,全局状态每变一次,监听它的子树都跟着重建;重建成本的分析思路与《React 渲染与重渲染》一致,谁订阅谁重建。
导航
组内共性:命令式(压栈弹栈)与声明式(页面是路由状态的函数)两条路线。官方当前建议:多数应用尤其带深链或 Web 目标时用 go_router,纯移动端简单跳转用命令式 Navigator 也完全够。
Navigator.push / pop(命令式)
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
);
// 返回时带值:Navigator.pop(context, 'ok')
一句话定位:把页面压栈、弹栈,await 还能拿返回值;最少代码完成跳转。
⚠️ 坑:Web 上没有”栈”,浏览器前进后退、刷新、分享链接都与 push 的栈对不齐——这是该换 go_router 的信号,不是要坚持修 Navigator 的理由。
Navigator 2 / Router(声明式)
一句话定位:页面列表 = f(路由状态),为深链与 Web 而生;直接裸写 RouterDelegate 与 RouteInformationParser 的样板量出名地大。
⚠️ 坑:它是 go_router 的地基而不是给应用开发者的日常 API;除非做高度定制的路由器,否则不裸用。
go_router
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (c, s) => const HomePage()),
GoRoute(
path: '/todo/:id', // 路径参数都是 String
builder: (c, s) => DetailPage(id: s.pathParameters['id']!),
),
],
);
// MaterialApp.router(routerConfig: router)
// 跳转:context.go('/todo/3') 替换栈;context.push('/todo/3') 压栈
一句话定位:Flutter 团队维护的路由包,路由表、声明式重定向、嵌套路由、深链一站式解决,是 Navigator 2 的易用封装。安装:flutter pub add go_router。
⚠️ 坑:pathParameters/queryParameters 拿到的都是 String,数字与布尔要自己解析校验;context.go 是”栈替换”、context.push 是”压栈”,混用时返回行为不一致。
快捷键与调试
组内共性:调试主入口是 DevTools 面板,快捷键围绕热重载;静态防线是 flutter analyze。
热重载 / 热重启
| 操作 | flutter run 终端 | 效果边界 |
|---|---|---|
| 热重载 | r(IDE 里保存文件即触发) | 保留 State 与导航;build 内的改动生效 |
| 热重启 | R | 丢 State 回首页;State 初始值、main 改动要它才生效 |
| 退出 | q | 改原生代码、依赖后需重跑 flutter run |
⚠️ 坑:把”热重载没反应”当 bug——先判断改动属于哪一层(界面 / 初始化 / 原生),边界见上表。
DevTools(Widget Inspector / Performance / Memory)
一句话定位:flutter run 时自动起服务,浏览器打开面板;Inspector 看 Widget 树与每个节点的实际约束,Performance 看帧曲线,Memory 抓内存泄漏。
⚠️ 坑:测性能要用 flutter run --profile——debug 模式自带的断言与检查开销会把帧率拉低到失真;release 模式则连不上 DevTools。
flutter analyze / dart fix
flutter analyze # 静态检查,提交前与 CI 里都该跑
dart fix --dry-run # 预览可自动修复的 lint 与弃用 API 用法
一句话定位:analyze 是 IDE 波浪线的命令行版本,适合当 CI 闸门;dart fix 自动迁移过时写法。
⚠️ 坑:只靠 IDE 提示不看 analyze——CI 与队友的 IDE 插件配置不同,闸门要立在仓库层面。
debugPrint 与 print
一句话定位:debugPrint 会截断超长输出避免被平台日志系统整行丢弃,是调试打印的正确默认。
⚠️ 坑:print 长字符串在部分平台直接丢内容;两类都别进生产代码,正式日志用日志库。
常见报错速查
组内共性:报错按来源分三类——生命周期违规、布局约束冲突、树结构违规。Flutter 的报错信息自带修复建议与文档链接,先读第一段再动手,不要见红就搜。
| 报错(节选关键词) | 触发场景 | 修法 |
|---|---|---|
| setState() called after dispose() | 异步回调回来时页面已销毁 | 回调里先查 if (!mounted) return; 再 setState |
| setState() called during build | build 过程里同步调了 setState | 挪到事件回调;确实要”build 后立刻改”用 addPostFrameCallback |
| A RenderFlex overflowed by N pixels | Row/Column 子项超出可用空间,屏幕现黄黑条纹 | 让某个子项 Expanded/Flexible,或包进可滚动容器 |
| Vertical viewport was given unbounded height | ListView 放在 Column 或无界高度里 | 外面包 Expanded 给定高度;确实要”占内容高度”用 shrinkWrap(注意性能) |
| Incorrect use of ParentDataWidget | Expanded 用在非 Flex 父级、Positioned 用在非 Stack 父级 | 检查直接父级类型,中间别隔别的 Widget |
| Looking up a deactivated widget’s ancestor is unsafe | 异步间隙后使用过期的 context | mounted 检查后重新取 context 再用 |
| Duplicate GlobalKey | 同一个 GlobalKey 挂到树上两处 | GlobalKey 全局唯一;多数场景 ValueKey 够用 |
| 列表滚动卡顿(非报错) | ListView children 全量构建、缺 const 构造 | 换 ListView.builder;构造加 const 减少重建对象 |
速查表
| 条目 | 一句话用途 | 有什么坑 |
|---|---|---|
| StatelessWidget | 纯配置组件,参数决定输出 | 持有可变字段无人通知重建 |
| StatefulWidget | 状态跨帧保留 | 改数据不包 setState 界面不动 |
| State 生命周期 | initState → build → dispose | 副作用放 build 会翻倍执行 |
| Container | 装饰盒子组合 | width 与 constraints 打架 |
| Row / Column | 主轴排列子项 | 无界约束与溢出高发 |
| Stack | 子项叠放定位 | Positioned 不参与尺寸 |
| Expanded / Flexible | 分配剩余空间,tight/loose 之分 | 内部约束变无限 |
| ListView(children:) | 少量内容全量构建 | 长列表掉帧 |
| ListView.builder | 惰性构建长列表 | itemCount、itemBuilder 副作用 |
| setState | 局部状态最小方案 | dispose 后与 build 中调用报错 |
| InheritedWidget | 子树只读共享数据 | 只分发不触发更新 |
| Provider / Riverpod | 共享状态与依赖注入 | 局部状态不必全局化 |
| Navigator.push/pop | 命令式压栈弹栈 | Web 深链对不齐 |
| Navigator 2 / Router | 声明式路由地基 | 样板多,不裸用 |
| go_router | 官方路由包,深链嵌套一站式 | 参数全是 String 要自查 |
| 热重载 r / 热重启 R | 秒级改代码反馈 | 初始值改动需 R |
| DevTools | 树 / 帧率 / 内存检查 | 性能用 profile 模式看 |
| flutter analyze | 静态检查闸门 | 别只依赖 IDE 提示 |
| debugPrint | 安全的调试打印 | print 长输出会被截断丢弃 |
下一步
这一篇是 Flutter 入门系列的收尾参考:Widget 与生命周期的实感来自《第一个 Flutter 应用》,语言层的机制(null safety、isolate、模式匹配)在《Dart 语法要点》里展开,环境问题回《Flutter 环境搭建与工具链》查。字典的边界也要诚实:它覆盖入门到日常开发的查表需求,平台通道、自定义 sliver、动画系统这些深水区不在其列,用到时以官方文档为准。
版本核实(撰写时检索,2024-05-06):Flutter 当前稳定版 3.19.x(Dart 3.3),3.22 预计 5 月发布。go_router 为 Flutter 团队(flutter packages 仓库)维护,官方导航文档对多数应用推荐 go_router 或命令式 Navigator、不推荐命名路由;Provider 与 Riverpod 为社区主流方案;material/cupertino 仍内置在 SDK 中,本篇 package:flutter/material.dart 写法直接可用。包安装一律 flutter pub add <name> 不钉版本,API 细节以 docs.flutter.dev 与各包 changelog 为准。