Flutter 教学笔记
最近准备使用使用开发APP项目,为了方便记录,通过B站某教学视频和Claude简单整理了一份Dart语法资料。这里也给大家分享一下!!!
目录
- Material 基础
- MaterialApp · Scaffold
- 组件基础
- StatelessWidget · StatefulWidget · 生命周期
- 事件处理
- GestureDetector · 计数器 Demo
- 布局组件
- Container · Center · Align · Padding
- Column · Row · Flex · Expanded · Flexible
- Wrap · Stack · Positioned
- 常用组件
- Text · TextField · Image
- 滚动组件
- SingleChildScrollView · ListView · GridView · CustomScrollView · PageView
- 组件通信
- 父传子 · 子传父 · InheritedWidget · Provider · EventBus · Bloc
一、Material 基础
Material 库
- Material 是 Google 推行的一套设计风格,涵盖颜色、文字排版、动画等规范
- 目标:为 Android、Web、iOS、HarmonyOS 多平台提供统一的交互和视觉体验
MaterialApp
整个应用被 MaterialApp 包裹,可对全局属性进行统一设计。
| 属性 | 说明 |
|---|---|
title | 窗口标题 |
theme | 全局主题 |
home | 首页(主体内容) |
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Flutter 组件初体验',
theme: ThemeData(scaffoldBackgroundColor: Colors.blue),
home: Scaffold(),
));
}
Scaffold
构建 Material Design 风格页面的核心骨架组件。
| 属性 | 主要作用 |
|---|---|
appBar | 顶部应用栏,显示标题、导航按钮和操作菜单 |
body | 页面主要内容区域 |
bottomNavigationBar | 底部导航栏 |
backgroundColor | 整体背景色 |
floatingActionButton | 悬浮操作按钮 |
Scaffold(
appBar: AppBar(title: Text('头部')),
body: Center(child: Text('中间区域')),
bottomNavigationBar: SizedBox(
height: 80,
child: Center(child: Text('底部')),
),
)
二、组件基础
StatelessWidget vs StatefulWidget
| 特性 | StatelessWidget(无状态) | StatefulWidget(有状态) |
|---|---|---|
| 核心特征 | 创建后内部状态不可变 | 持有可在生命周期内改变的状态 |
| 使用场景 | 静态内容展示,外观仅由配置参数决定 | 交互式组件,如计数器、开关、表单输入框 |
| 生命周期 | 相对简单,主要是 build() | 更为复杂,包含创建、更新和销毁 |
| 代码结构 | 单个类 | 两个关联类:Widget 本身和单独的 State 类 |
VS Code 快捷键:
- 无状态组件:
statelessW(Awesome Flutter Snippets 插件) - 有状态组件:
statefulW
StatelessWidget
class MyCard extends StatelessWidget {
const MyCard({super.key});
@override
Widget build(BuildContext context) {
return Container();
}
}
StatefulWidget
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
@override
Widget build(BuildContext context) {
return Container();
}
}
生命周期
完整执行顺序:
| 顺序 | 方法 | 执行次数 | 调用时机 | 适合做的事 |
|---|---|---|---|---|
| 1 | createState() | 一次 | Widget 初始化时 | 创建 State 对象 |
| 2 | initState() | 一次 | State 插入树后立刻 | 初始化变量、创建 Controller、订阅 Stream、首次网络请求 |
| 3 | didChangeDependencies() | 多次 | initState 后 + InheritedWidget 变化时 | 读取 Theme.of(context)、MediaQuery.of(context) |
| 4 | build() | 多次 | 初始化或状态更新后 | 只做 UI 构建,不放耗时操作,不调用 setState() |
| 5 | setState() | 多次 | 主动标记状态变更 | 更新计数器、切换选中状态、修改列表数据 |
| 6 | didUpdateWidget() | 多次 | 父组件传入新配置时 | 对比新旧 widget 属性差异、重置 Controller、重新发起请求 |
| 7 | reassemble() | 多次 | 调试 Hot Reload 时 | 仅调试用,生产不执行 |
| 8 | deactivate() | 多次 | State 从树中暂时移除时 | 暂停动画、保存临时草稿 |
| 9 | dispose() | 一次 | State 永久移除时 | 释放 Controller、取消 Stream 订阅、取消网络请求、清理定时器 |
🟢 组件创建
│
① createState()
│
② initState()
│
③ didChangeDependencies() ◄── InheritedWidget 更新时重复触发
│
④ build() ◄──────────────────────────────────────────┐
│ │
┌──────┼──────────────────────┐ │
▼ ▼ ▼ │
⑤ setState() ⑥ didUpdateWidget() ⑦ reassemble() │
└──────┴──────────────────────┘ ──触发重建──────────┘
│
组件被移除
│
⑧ deactivate()(暂时移除,可能重新插入)
│
⑨ dispose()(永久移除,释放资源)
│
🔴 组件销毁
核心规律: 凡是能改变状态或配置的操作,最终都会回到 build(),这是 Flutter UI 更新的唯一入口。
InheritedWidget: 专门用于在 Widget 树中自顶向下高效共享数据。Flutter 内置的
Theme、MediaQuery底层都基于它实现。
三、事件处理
GestureDetector
Flutter 中最常用、功能最丰富的手势检测组件,通过包裹目标组件添加交互。
GestureDetector(
onTap: () => print('点击'),
onDoubleTap: () => print('双击'),
onLongPress: () => print('长按'),
child: Text('可点击区域'),
)
其他常用交互组件:
| 组件类别 | 核心组件 | 使用场景 |
|---|---|---|
| 专用按钮组件 | ElevatedButton、TextButton、OutlinedButton | 内置点击动画和样式 |
| 视觉反馈组件 | InkWell | Material 水纹扩散效果 |
| 其他交互组件 | IconButton、Switch、Checkbox | 具有特定功能的交互控件 |
计数器 Demo
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
// State 字段只初始化一次,build() 重建多少次 count 都保持不变
// 等价于 React 的 useState(0)
int count = 0;
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Row(
children: [
TextButton(
onPressed: () => setState(() => count++), // 等价于 setCount(count + 1)
child: Text('加1'),
),
Text('$count'),
TextButton(
onPressed: () => setState(() => count--),
child: Text('减1'),
),
],
),
),
),
);
}
}
四、布局组件
Flutter 提供了丰富的布局组件:
| 组件类别 | 核心组件 | 主要特点 |
|---|---|---|
| 基础容器 | Container、Center、Align、Padding、SizedBox | 装饰、对齐、边距、固定尺寸 |
| 线性布局 | Row、Column | 水平或垂直线性排列 |
| 弹性布局 | Flex、Expanded、Flexible | 按比例分配剩余空间 |
| 层叠布局 | Stack、Positioned | 子组件重叠堆叠 |
| 流式布局 | Wrap | 主轴空间不足时自动换行 |
| 滚动布局 | ListView、GridView、SingleChildScrollView | 可滚动列表、网格或整页视图 |
| 约束布局 | SafeArea、LayoutBuilder | 避开系统 UI / 根据父约束动态构建 |
Container
基础布局容器,可施加样式、布局约束和变换。
尺寸优先级: 明确宽高 > constraints 约束 > 父组件约束 > 自适应组件大小
| 属性类别 | 关键属性 | 作用说明 |
|---|---|---|
| 布局定位 | alignment | 子组件在容器内的对齐方式 |
| 尺寸控制 | width / height / constraints | 设置宽高或约束 |
| 间距留白 | padding / margin | 内边距 / 外边距 |
| 装饰效果 | color / decoration | 背景色 / 复杂装饰(两者互斥) |
| 变换效果 | transform | 矩阵变换(旋转、倾斜、平移) |
Container(
transform: Matrix4.rotationZ(0.05), // 弧度,非角度
margin: EdgeInsets.all(20),
alignment: Alignment.center,
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(15),
border: Border.all(width: 3, color: Colors.amber),
),
child: Text('Hello', style: TextStyle(color: Colors.white, fontSize: 20)),
)
⚠️ color 与 decoration 互斥
color参数本质是BoxDecoration(color: color)的语法糖。同时设置两者会抛出:Cannot provide both a color and a decoration需要圆角、边框等复杂样式时,把颜色写在decoration里。
Container 是纯语法糖组件,所有参数都有对应的底层 Widget:
| Container 参数 | 等价底层 Widget | 说明 |
|---|---|---|
color / decoration | DecoratedBox | 背景色、圆角、边框 |
padding | Padding | 内边距 |
alignment | Align | 子组件对齐 |
width / height | SizedBox | 固定尺寸 |
constraints | ConstrainedBox | 最大/最小尺寸约束 |
transform | Transform | 矩阵变换 |
实际建议: 只需要单一功能时优先用底层 Widget(只需间距用 Padding,只需固定尺寸用 SizedBox),性能更好也更语义化。需要组合多个样式时再用 Container。
Center
将子组件在父容器内水平和垂直居中。
Center本身不能设置宽高,其大小取决于父组件约束- 等价于
Align(alignment: Alignment.center, ...)
Center(
child: Container(
color: Colors.blue,
child: Text('居中'),
),
)
Align
精确控制子组件在父容器内的对齐位置。
| 属性 | 说明 |
|---|---|
alignment | 对齐方式,如 Alignment.bottomRight |
widthFactor | Align 宽度 = 子组件宽度 × 该因子 |
heightFactor | Align 高度 = 子组件高度 × 该因子 |
Align(
alignment: Alignment.bottomRight, // topLeft/topCenter/topRight/centerLeft/center/centerRight/bottomLeft/bottomCenter/bottomRight
child: Icon(Icons.star),
)
Padding
为子组件添加内边距。
// 四边相同
Padding(padding: EdgeInsets.all(20), child: ...)
// 单独方向
Padding(padding: EdgeInsets.only(left: 40, top: 10), child: ...)
// 对称方向
Padding(padding: EdgeInsets.symmetric(horizontal: 50, vertical: 20), child: ...)
// 上右下左各自指定(fromLTRB 参数顺序: left, top, right, bottom)
Padding(padding: EdgeInsets.fromLTRB(10, 30, 60, 10), child: ...)
Column
垂直排列子组件,等价于 CSS display: flex; flex-direction: column。
| 属性 | CSS 等价 | 说明 |
|---|---|---|
mainAxisAlignment | justify-content | 主轴(垂直)对齐:start/end/center/spaceBetween/spaceAround/spaceEvenly |
crossAxisAlignment | align-items | 交叉轴(水平)对齐:start/end/center/stretch |
mainAxisSize | height | max=占满父级(默认),min=包裹内容 |
// React
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
<div>item1</div>
</div>
// Flutter
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [Text('item1'), Text('item2')],
)
注意事项:
- Column 本身不支持滚动,内容超出时用
SingleChildScrollView或ListView包裹 - 子组件总高度超出会报 overflow 错误,用
Expanded或滚动组件解决
Row
水平排列子组件,等价于 CSS display: flex; flex-direction: row。
| 属性 | CSS 等价 | 说明 |
|---|---|---|
mainAxisAlignment | justify-content | 主轴(水平)对齐:start/end/center/spaceBetween/spaceAround/spaceEvenly |
crossAxisAlignment | align-items | 交叉轴(垂直)对齐:start/end/center/stretch |
mainAxisSize | width | max=占满父级(默认),min=包裹内容 |
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text('左'), Text('右')],
)
注意: 子组件总宽度超过 Row 宽度会报 overflow 错误,用 Expanded 让某个子组件自适应解决。
Flex / Expanded / Flexible
Flex 是 Row 和 Column 的底层实现,通过 direction 控制方向。实际开发中直接用 Row / Column,Flex 用在需要动态切换方向时。
Expanded vs Flexible:
Expanded | Flexible | |
|---|---|---|
| CSS 等价 | flex: 1(强制填满) | flex: 1; flex-shrink: 1(不强制) |
| 内容小时 | 强制撑满分配空间 | 仅占内容实际大小 |
| 使用场景 | 输入框、表格列 | 价格标签、弹性文字 |
Row(
children: [
Expanded(flex: 2, child: Container(color: Colors.blue)), // 占 2/3
Expanded(flex: 1, child: Container(color: Colors.green)), // 占 1/3
],
)
常见场景:
// 搜索栏:输入框自适应 + 按钮固定宽
Row(
children: [
Expanded(child: TextField()), // 自适应宽度
SizedBox(width: 80, child: ElevatedButton(child: Text('搜索'))),
],
)
// 固定头尾 + 中间自适应(Sticky Footer)
Column(
children: [
Container(height: 40, color: Colors.blue, child: Text('固定头')),
Expanded(child: Text('中间内容')), // 撑满剩余高度
Container(height: 40, color: Colors.red, child: Text('固定底')),
],
)
Wrap
流式布局,子组件排满一行后自动换行,等价于 CSS display: flex; flex-wrap: wrap。
| 属性 | CSS 等价 | 说明 |
|---|---|---|
spacing | column-gap | 主轴方向子组件间距 |
runSpacing | row-gap | 行间距 |
alignment | justify-content | 主轴方向对齐(WrapAlignment) |
runAlignment | align-content | 多行整体在交叉轴的对齐 |
direction | flex-direction | Axis.horizontal(默认) |
Wrap(
spacing: 8,
runSpacing: 8,
children: tags.map((tag) => Chip(label: Text(tag))).toList(),
)
适用场景: 标签云、筛选条件、相册缩略图等数量不固定的内容。
Stack 和 Positioned
层叠布局,允许子组件重叠显示,等价于 CSS position: relative + position: absolute。
| 属性 | 说明 |
|---|---|
Stack alignment | 未使用 Positioned 的子组件默认对齐(默认 topLeft) |
Stack clipBehavior | 是否裁剪超出边界的子组件 |
Positioned top/left/right/bottom | 距 Stack 四边的距离 |
Positioned.fill | 铺满整个 Stack(top:0; left:0; right:0; bottom:0) |
Stack(
children: [
// 底层:整张图
Image.network(url, fit: BoxFit.cover),
// 左上角角标
Positioned(
top: 8, left: 8,
child: Container(color: Colors.red, child: Text('NEW')),
),
// 底部蒙层文字
Positioned(
left: 0, right: 0, bottom: 0,
child: Container(color: Colors.black54, child: Text('商品名称')),
),
],
)
层级规则: children 列表中越靠后的 Widget 层级越高(类似 CSS z-index 越大)。
注意事项:
- Stack 尺寸由最大的非 Positioned 子组件决定
- Positioned 必须是 Stack 的直接子组件
- 绝对居中用
Positioned.fill+Align(alignment: Alignment.center)组合
五、常用组件
Text
显示文本,支持样式、富文本、溢出处理。
| 属性 | CSS 等价 |
|---|---|
style: TextStyle(fontSize: 16) | font-size: 16px |
style: TextStyle(color: Colors.red) | color: red |
style: TextStyle(fontWeight: FontWeight.bold) | font-weight: bold |
style: TextStyle(letterSpacing: 2) | letter-spacing: 2px |
style: TextStyle(height: 1.5) | line-height: 1.5 |
style: TextStyle(decoration: TextDecoration.underline) | text-decoration: underline |
textAlign: TextAlign.center | text-align: center |
maxLines: 2 | - |
overflow: TextOverflow.ellipsis | text-overflow: ellipsis |
// 基础样式
Text('粗体红色', style: TextStyle(fontSize: 20, color: Colors.red, fontWeight: FontWeight.bold))
// 超出省略(overflow 必须配合 maxLines 才生效)
Text('很长很长的内容...', maxLines: 1, overflow: TextOverflow.ellipsis)
// 富文本:混合不同样式
Text.rich(
TextSpan(
text: '价格:',
style: TextStyle(color: Colors.grey),
children: [
TextSpan(text: '¥99', style: TextStyle(fontSize: 22, color: Colors.red, fontWeight: FontWeight.bold)),
TextSpan(text: ' 起', style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
)
decoration 可选值: none / underline / lineThrough / overline
overflow 可选值: ellipsis(省略号)/ clip(裁剪)/ fade(淡出)
TextField
文本输入框,等价于 HTML <input> 和 <textarea>。
| 属性 | HTML 等价 | 说明 |
|---|---|---|
controller | ref + value | TextEditingController,读取/设置/监听 |
decoration | CSS 样式 | 输入框外观(边框、提示文字、前后图标) |
hintText(在 decoration 里) | placeholder | 占位提示 |
labelText(在 decoration 里) | <label> | 浮动标签 |
obscureText: true | type="password" | 密码遮罩 |
keyboardType | type="number" | 弹出键盘类型 |
maxLines | rows | 最大行数(>1 变为多行文本框) |
maxLength | maxlength | 最大字符数,自动显示计数器 |
enabled: false | disabled | 禁用 |
readOnly: true | readonly | 只读 |
onChanged | onChange | 输入变化回调 |
inputFormatters | oninput 过滤 | 限制输入内容(如只允许数字) |
keyboardType 常用值: text / number / phone / emailAddress / url / multiline
// 带边框的输入框
TextField(
decoration: InputDecoration(
labelText: '用户名',
hintText: '请输入用户名',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
)
// 密码输入框(可显示/隐藏)
class _PasswordField extends StatefulWidget { ... }
class _PasswordFieldState extends State<_PasswordField> {
bool _obscure = true;
@override
Widget build(BuildContext context) {
return TextField(
obscureText: _obscure,
decoration: InputDecoration(
labelText: '密码',
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _obscure = !_obscure),
),
),
);
}
}
// 只允许输入数字
TextField(
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)
TextEditingController 使用:
final controller = TextEditingController();
// 读取值
print(controller.text);
// 设置值
controller.text = 'hello';
// 必须在 dispose() 中释放!
@override
void dispose() {
controller.dispose();
super.dispose();
}
注意事项:
TextField本身无验证,需要验证时用TextFormField+FormmaxLines: null表示无限行数(自动增高)controller必须在dispose()中调用.dispose()
Image
显示图片,支持网络图、本地资源图、内存图等多种来源。
| 构造方式 | 说明 |
|---|---|
Image.network(url) | 网络图片 |
Image.asset('...') | 本地资源图(需在 pubspec.yaml 注册) |
Image.file(File(...)) | 本地文件图 |
Image.memory(bytes) | 内存中的图片数据 |
| 属性 | CSS 等价 | 说明 |
|---|---|---|
fit: BoxFit.cover | object-fit: cover | 裁剪填满,最常用 |
fit: BoxFit.contain | object-fit: contain | 保持比例完全显示 |
fit: BoxFit.fill | object-fit: fill | 拉伸填满(可能变形) |
loadingBuilder | - | 加载中占位 Widget |
errorBuilder | - | 加载失败占位 Widget |
// 网络图片 + 加载/错误处理
Image.network(
url,
fit: BoxFit.cover,
loadingBuilder: (_, child, progress) =>
progress == null ? child : Center(child: CircularProgressIndicator()),
errorBuilder: (_, __, ___) => Icon(Icons.broken_image, size: 48),
)
// 圆形图片(ClipOval)
ClipOval(
child: Image.network(url, width: 80, height: 80, fit: BoxFit.cover),
)
// 圆角图片(ClipRRect)
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(url, fit: BoxFit.cover),
)
// 圆形头像(推荐用 CircleAvatar)
Row(children: [
CircleAvatar(radius: 30, backgroundColor: Colors.blue, child: Text('张')), // 文字头像
CircleAvatar(radius: 30, backgroundImage: NetworkImage(url)), // 图片头像
])
注意事项:
- 本地图片必须在
pubspec.yaml的flutter.assets中注册 - 生产环境推荐
cached_network_image包,自动缓存避免重复下载 loadingBuilder和errorBuilder是生产环境必须处理的两个状态
六、滚动组件
SingleChildScrollView
为单个子 Widget 提供可滚动容器,适合页面整体滚动,不适合大量数据列表。
| 属性 | 说明 |
|---|---|
scrollDirection | Axis.vertical(默认)/ Axis.horizontal |
controller | ScrollController,编程控制滚动位置 |
physics | BouncingScrollPhysics()(回弹)/ NeverScrollableScrollPhysics()(禁止) |
reverse | 反向滚动 |
SizedBox(
height: 300,
child: SingleChildScrollView(
child: Column(children: [...]),
),
)
ScrollController 常用方法:
// 带动画滚动
controller.animateTo(offset, duration: Duration(milliseconds: 400), curve: Curves.easeIn);
// 立即跳转
controller.jumpTo(offset);
// 滚动到底部
controller.animateTo(controller.position.maxScrollExtent, ...);
注意事项:
ScrollController必须在dispose()中释放- 大量数据(>50 条)务必用
ListView.builder替代 Column套在SingleChildScrollView里时,Column不能设置mainAxisSize: MainAxisSize.max
ListView
可滚动列表,支持懒加载,是处理列表数据的核心组件。
三种变体对比
| 场景 | 用哪个 |
|---|---|
| 固定少量 item | ListView |
| 大量/动态数据 | ListView.builder |
| 需要分隔线/间距 | ListView.separated |
ListView — 静态/小数据量
ListView(
children: [
ListTile(leading: Icon(Icons.person), title: Text('个人信息')),
ListTile(leading: Icon(Icons.lock), title: Text('修改密码')),
ListTile(leading: Icon(Icons.notifications), title: Text('消息通知')),
ListTile(leading: Icon(Icons.logout), title: Text('退出登录')),
],
)
- 直接传
children数组,一次性渲染所有子项 - 适合固定少量内容(推荐 20 条以内)
💡 为什么不用
builder? 菜单项固定 4 个,直接传children最简洁。builder的按需渲染优势只在数据量大时体现。 面试点:ListView一次性渲染所有子项,超过 20 条建议换builder。
ListView.builder — 动态/大数据量
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) => ListTile(title: Text('item $index')),
)
- 按需渲染,只渲染可见区域的 item(通常 10~15 个)
- 滚动到哪渲染到哪,超出屏幕的自动销毁
itemCount不传则无限滚动
💡 为什么必须用
builder? 1000 个 item 全部创建会造成严重卡顿甚至 OOM(内存溢出)崩溃。 面试点:ListView.builder内部用SliverList实现懒加载,是 Flutter 处理长列表的标准做法。
ListView.separated — 带分隔线
ListView.separated(
itemCount: 20,
itemBuilder: (context, index) => ListTile(title: Text('item $index')),
separatorBuilder: (context, index) => Divider(),
)
- 在
builder基础上多了separatorBuilder - 自动不在最后一项后面添加分隔
- 分隔符可以是任意 Widget,不只是线
💡 为什么不在
itemBuilder里手动加分隔? 手动加需要判断是否是最后一项,逻辑分散容易出错。separated职责清晰,且自动处理最后一项。 面试点:separated的 item 实际数量是itemCount * 2 - 1(item + separator 交替)。
完整演示代码
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: MainPage(),
));
}
class MainPage extends StatelessWidget {
const MainPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
title: const Text('ListView'),
bottom: const TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
tabs: [
Tab(text: 'children'),
Tab(text: 'builder'),
Tab(text: 'separated'),
Tab(text: '横向 + 下拉刷新'),
],
),
),
body: const TabBarView(
children: [
_ListViewChildren(),
_ListViewBuilder(),
_ListViewSeparated(),
_ListViewHorizontal(),
],
),
),
);
}
}
class _ListViewChildren extends StatelessWidget {
const _ListViewChildren();
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(12),
children: List.generate(
5,
(i) => Container(
margin: const EdgeInsets.only(bottom: 8),
height: 60,
color: Colors.blue,
alignment: Alignment.center,
child: Text('item $i', style: const TextStyle(color: Colors.white)),
),
),
);
}
}
class _ListViewBuilder extends StatelessWidget {
const _ListViewBuilder();
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: 100,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
height: 60,
color: Colors.primaries[index % Colors.primaries.length],
alignment: Alignment.center,
child: Text('item $index', style: const TextStyle(color: Colors.white)),
);
},
);
}
}
class _ListViewSeparated extends StatelessWidget {
const _ListViewSeparated();
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: 20,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue,
child: Text('${index + 1}', style: const TextStyle(color: Colors.white)),
),
title: Text('列表项 ${index + 1}'),
subtitle: const Text('副标题内容'),
trailing: const Icon(Icons.arrow_forward_ios, size: 14),
onTap: () {},
);
},
);
}
}
class _ListViewHorizontal extends StatefulWidget {
const _ListViewHorizontal();
@override
State<_ListViewHorizontal> createState() => _ListViewHorizontalState();
}
class _ListViewHorizontalState extends State<_ListViewHorizontal> {
int _count = 10;
@override
Widget build(BuildContext context) {
return Column(
children: [
const Padding(
padding: EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Text('横向滚动', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)),
),
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: 10,
itemBuilder: (_, i) => Container(
width: 80,
margin: const EdgeInsets.only(right: 8),
color: Colors.primaries[i % Colors.primaries.length],
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(color: Colors.white)),
),
),
),
const SizedBox(height: 16),
const Padding(
padding: EdgeInsets.fromLTRB(12, 0, 12, 4),
child: Text('下拉刷新(RefreshIndicator)', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)),
),
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await Future.delayed(const Duration(seconds: 1));
setState(() => _count += 5);
},
child: ListView.builder(
itemCount: _count,
itemBuilder: (_, i) => ListTile(
title: Text('item $i'),
leading: const Icon(Icons.inbox),
),
),
),
),
],
);
}
}
GridView
网格布局列表,等价于 CSS display: grid。
三种变体对比
| 场景 | 用哪个 |
|---|---|
| 固定几列 | GridView.count |
| 自适应屏幕宽度 | GridView.extent |
| 数据量大 | GridView.builder |
共有属性
| 属性 | 归属 | 效果 |
|---|---|---|
crossAxisCount | count 专属 | 固定每行列数 |
maxCrossAxisExtent | extent 专属 | 每项最大宽度,列数自动算 |
mainAxisSpacing | 三种共有 | 主轴(垂直)间距 |
crossAxisSpacing | 三种共有 | 交叉轴(水平)间距 |
childAspectRatio | 三种共有 | 宽高比,1.0=正方形,<1变高,>1变宽 |
padding | 三种共有 | 整体内边距 |
scrollDirection | 三种共有 | 滚动方向,默认 Axis.vertical |
shrinkWrap | 三种共有 | 是否包裹内容高度,嵌套滚动时用 |
GridView.count — 固定每行列数
GridView.count(
crossAxisCount: 3, // [count 专属] 每行3列
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0, // 1.0=正方形,<1 变高,>1 变宽
children: apps.map((name) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [Icon(Icons.apps, size: 48, color: Colors.blue), Text(name)],
)).toList(),
)
💡 什么时候用
count? 设计稿明确要求"每行N个",列数是固定的产品需求。 面试点:count内部用的是SliverGridDelegateWithFixedCrossAxisCount。
GridView.extent — 固定每项最大宽度,自动算列数
GridView.extent(
maxCrossAxisExtent: 150, // [extent 专属] 每项最宽150,列数=屏幕宽/150
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0,
children: [...],
)
- 屏幕 600 宽 → 600/150 = 4 列;屏幕 300 宽 → 300/150 = 2 列
- 适合响应式布局,自动适配不同屏幕
💡 什么时候用
extent? 内容在手机、平板、折叠屏上运行,需要根据屏幕宽度自动调整列数。 面试点:extent适合响应式场景,是平板/Web 适配的常用手段。extent和builder不互斥,builder的gridDelegate可用WithMaxCrossAxisExtent,同时拥有自适应列数和懒加载。
GridView.builder — 大数据量,按需渲染
GridView.builder(
itemCount: 1000,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 0.75,
),
itemBuilder: (context, index) => Card(
child: Column(
children: [
Expanded(child: Image.network('https://picsum.photos/200?random=$index', fit: BoxFit.cover)),
Text('商品${index + 1}'),
Text('¥${(index + 1) * 9}', style: TextStyle(color: Colors.red)),
],
),
),
)
- 只渲染可见区域,性能最好
gridDelegate有两种:SliverGridDelegateWithFixedCrossAxisCount→ 固定列数(对应 count)SliverGridDelegateWithMaxCrossAxisExtent→ 固定最大宽度(对应 extent)
💡 1000 条商品为什么必须用
builder?count和extent的children列表意味着所有 item 进入页面时全部创建,内存瞬间暴涨必然卡顿甚至崩溃。 面试点:GridView.builder和ListView.builder都基于SliverMultiBoxAdaptorElement实现元素复用,原理和 AndroidRecyclerView、iOSUICollectionView的 cell 复用一致。
完整演示代码
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: MainPage(),
));
}
class MainPage extends StatelessWidget {
const MainPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
title: const Text('GridView'),
bottom: const TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
tabs: [
Tab(text: 'count'),
Tab(text: 'extent'),
Tab(text: 'builder'),
Tab(text: '响应式列数'),
],
),
),
body: const TabBarView(
children: [
_GridCount(),
_GridExtent(),
_GridBuilder(),
_GridResponsive(),
],
),
),
);
}
}
class _GridCount extends StatelessWidget {
const _GridCount();
@override
Widget build(BuildContext context) {
return GridView.count(
padding: const EdgeInsets.all(12),
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1,
children: List.generate(
12,
(i) => Container(
color: Colors.primaries[i % Colors.primaries.length],
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(color: Colors.white, fontSize: 18)),
),
),
);
}
}
class _GridExtent extends StatelessWidget {
const _GridExtent();
@override
Widget build(BuildContext context) {
return GridView.extent(
padding: const EdgeInsets.all(12),
maxCrossAxisExtent: 120,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1,
children: List.generate(
20,
(i) => Container(
color: Colors.primaries[i % Colors.primaries.length],
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(color: Colors.white, fontSize: 18)),
),
),
);
}
}
class _GridBuilder extends StatelessWidget {
const _GridBuilder();
@override
Widget build(BuildContext context) {
return GridView.builder(
padding: const EdgeInsets.all(12),
itemCount: 100,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (context, i) => Container(
color: Colors.primaries[i % Colors.primaries.length],
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(color: Colors.white)),
),
);
}
}
class _GridResponsive extends StatelessWidget {
const _GridResponsive();
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final columns = (constraints.maxWidth / 120).floor().clamp(2, 6);
return GridView.builder(
padding: const EdgeInsets.all(12),
itemCount: 30,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemBuilder: (_, i) => Container(
color: Colors.primaries[i % Colors.primaries.length],
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$i', style: const TextStyle(color: Colors.white, fontSize: 16)),
Text('cols: $columns', style: const TextStyle(color: Colors.white70, fontSize: 10)),
],
),
),
);
},
);
}
}
CustomScrollView
自定义滚动视图,通过 Sliver 组件把多种内容(列表、网格、Banner、吸顶 Header)统一在一个滚动容器中。
为什么需要它?
ListView、GridView 无法混合在同一个滚动区域里。
💡 为什么不在 Column 里嵌套 ListView?
Column里嵌套ListView会报 "unbounded height" 错误,因为ListView需要固定高度的父容器。即使加了shrinkWrap: true,会一次性渲染所有子项,失去懒加载优势。 面试点: 面试官常问"如何在列表顶部加一个 Banner",错误答案是Column + ListView,正确答案是CustomScrollView + SliverToBoxAdapter + SliverList。
Sliver 是什么?
Sliver 是能感知滚动位置的特殊 Widget,只能用在 CustomScrollView 的 slivers 里。
普通 Widget(Container、Text) → 用 SliverToBoxAdapter 包裹后放入
滚动类 Widget(ListView、Grid) → 用对应的 Sliver 版本替换
普通 Widget vs Sliver 版本对照
| 普通写法 | Sliver 版本 | 说明 |
|---|---|---|
ListView.builder | SliverList.builder | 懒加载列表 |
ListView.separated | SliverList.separated | 带分隔线列表 |
GridView.count | SliverGrid.count | 网格 |
| 任意普通 Widget | SliverToBoxAdapter | 包裹普通 Widget |
| 吸顶 Header | SliverPersistentHeader | 需配合 delegate |
| 可折叠 AppBar | SliverAppBar | 内置折叠动效 |
SliverPersistentHeader — 吸顶 Header
SliverPersistentHeader(
pinned: true, // true = 滚动时吸顶,false = 滚出屏幕消失
delegate: _MyDelegate(),
)
class _MyDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(color: Colors.white, child: Text('分类'));
}
@override
double get maxExtent => 80; // 未吸顶时的高度(展开)
@override
double get minExtent => 40; // 吸顶后的高度(折叠)
// 与 maxExtent 相同则高度固定不变
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate old) => false;
// false = 内容不变不重建,性能优化
}
shrinkOffset是收缩偏移量(0 到 maxExtent-minExtent),可用来做渐变、缩放等滚动动画。
基础结构 + 完整演示
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: MainPage(),
));
}
class MainPage extends StatelessWidget {
const MainPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('CustomScrollView')),
body: const Body(),
);
}
}
class Body extends StatelessWidget {
const Body({super.key});
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
// ① 顶部可折叠大图(滚动时收缩)
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('SliverAppBar'),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue, Colors.purple],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Center(
child: Text('向上滚动查看折叠效果', style: TextStyle(color: Colors.white70)),
),
),
),
),
// ② 普通 Widget(轮播图、广告、任意内容)
SliverToBoxAdapter(
child: Container(
height: 60,
color: Colors.orange[100],
alignment: Alignment.center,
child: const Text('SliverToBoxAdapter — 普通 Widget 转 Sliver'),
),
),
// ③ 间距
const SliverToBoxAdapter(child: SizedBox(height: 8)),
// ④ 吸顶分类栏
SliverPersistentHeader(
pinned: true,
delegate: _StickyHeader(
child: Container(
color: Colors.white,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: const Text('吸顶分类标题', style: TextStyle(fontWeight: FontWeight.bold)),
),
),
),
// ⑤ 网格(对应 GridView.count)
SliverGrid.count(
crossAxisCount: 3,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: List.generate(6, (i) => Container(
color: Colors.primaries[i % Colors.primaries.length],
alignment: Alignment.center,
child: Text('$i', style: const TextStyle(color: Colors.white)),
)),
),
const SliverToBoxAdapter(child: SizedBox(height: 8)),
// ⑥ 列表(对应 ListView.builder)
SliverList.builder(
itemCount: 10,
itemBuilder: (_, i) => ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue,
child: Text('${i + 1}', style: const TextStyle(color: Colors.white)),
),
title: Text('SliverList 列表项 ${i + 1}'),
subtitle: const Text('与 SliverGrid 共享同一个滚动容器'),
),
),
// ⑦ 底部留白(防止最后一项被遮挡)
const SliverToBoxAdapter(child: SizedBox(height: 40)),
],
);
}
}
class _StickyHeader extends SliverPersistentHeaderDelegate {
final Widget child;
const _StickyHeader({required this.child});
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child;
@override
double get maxExtent => 48;
@override
double get minExtent => 48;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate old) => false;
}
使用场景总结
| 场景 | 用哪个 |
|---|---|
| 单纯列表或网格 | ListView / GridView |
| 混合多种内容(图+列表+网格) | CustomScrollView |
| 需要吸顶 Header | CustomScrollView + SliverPersistentHeader |
| 需要可折叠头图 | CustomScrollView + SliverAppBar |
PageView
页面级整屏滑动组件,适合轮播图或 App 引导页,每次滑动切换一整屏内容。
| 属性 | 说明 |
|---|---|
controller | PageController,控制页面跳转和读取当前页 |
onPageChanged | 页面切换回调,参数为新页面索引 |
itemCount | 总页数(.builder 版) |
scrollDirection | Axis.horizontal(默认)/ Axis.vertical 上下翻页 |
physics | BouncingScrollPhysics() 回弹 / NeverScrollableScrollPhysics() |
pageSnapping | true(默认)每次滑动整页吸附 |
PageController 方法:
// 带动画跳转
controller.animateToPage(index, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
// 立即跳转
controller.jumpToPage(index);
// 读取当前页(返回 double)
controller.page
自动轮播:
// initState 里启动
_timer = Timer.periodic(Duration(seconds: 3), (_) {
final next = (_current + 1) % total;
controller.animateToPage(next, duration: Duration(milliseconds: 400), curve: Curves.easeInOut);
});
// dispose() 里必须取消!
@override
void dispose() {
_timer?.cancel();
_controller.dispose();
super.dispose();
}
完整演示代码:
import 'dart:async';
import 'package:flutter/material.dart';
// 基础 PageView(带指示器)
class _BasicPageView extends StatefulWidget {
const _BasicPageView();
@override
State<_BasicPageView> createState() => _BasicPageViewState();
}
class _BasicPageViewState extends State<_BasicPageView> {
final _controller = PageController();
int _current = 0;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: PageView.builder(
controller: _controller,
itemCount: 4,
onPageChanged: (i) => setState(() => _current = i),
itemBuilder: (_, i) => Container(
color: Colors.primaries[i * 3 % Colors.primaries.length],
alignment: Alignment.center,
child: Text('Page ${i + 1}', style: const TextStyle(color: Colors.white, fontSize: 32)),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (i) => GestureDetector(
onTap: () => _controller.animateToPage(
i, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut,
),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: _current == i ? 20 : 8,
height: 8,
decoration: BoxDecoration(
color: _current == i ? Colors.blue : Colors.grey[300],
borderRadius: BorderRadius.circular(4),
),
),
)),
),
),
],
);
}
}
// 自动轮播图
class _AutoPageView extends StatefulWidget {
const _AutoPageView();
@override
State<_AutoPageView> createState() => _AutoPageViewState();
}
class _AutoPageViewState extends State<_AutoPageView> {
final _controller = PageController();
int _current = 0;
Timer? _timer;
static const _total = 5;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 2), (_) {
final next = (_current + 1) % _total;
_controller.animateToPage(next, duration: const Duration(milliseconds: 400), curve: Curves.easeInOut);
});
}
@override
void dispose() {
_timer?.cancel();
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: Stack(
children: [
PageView.builder(
controller: _controller,
itemCount: _total,
onPageChanged: (i) => setState(() => _current = i),
itemBuilder: (_, i) => Container(
color: Colors.primaries[i * 4 % Colors.primaries.length],
alignment: Alignment.center,
child: Text('轮播图 ${i + 1}', style: const TextStyle(color: Colors.white, fontSize: 24)),
),
),
Positioned(
bottom: 12, left: 0, right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_total, (i) => Container(
margin: const EdgeInsets.symmetric(horizontal: 3),
width: _current == i ? 16 : 6,
height: 6,
decoration: BoxDecoration(
color: _current == i ? Colors.white : Colors.white54,
borderRadius: BorderRadius.circular(3),
),
)),
),
),
],
),
);
}
}
七、组件通信
通信方式总览
| 通信方式 | 方向 | 适用场景 |
|---|---|---|
| 构造函数 | 父 → 子 | 简单数据传递,数据在编译时已知 |
| 回调函数 | 子 → 父 | 子通知父更新数据 |
| InheritedWidget | 祖先 → 后代 | 跨层级共享,Theme/MediaQuery 底层实现 |
| Provider | 任意方向 | 官方推荐,中小项目首选 |
| EventBus | 任意方向 | 完全不相关的组件间全局事件 |
| Bloc / Riverpod | 任意方向 | 大型项目复杂状态管理 |
学习顺序: 构造函数 → 回调函数 → Provider → EventBus → Bloc/Riverpod
父传子(构造函数)
和 React 的对比
| 对比项 | React | Flutter |
|---|---|---|
| 传参方式 | JSX 属性 <C title="x" /> | 构造函数具名参数 C(title: 'x') |
| 接收方式 | props 对象或解构 | 类的成员变量(final) |
| 必传校验 | PropTypes / TypeScript | required 关键字 |
| 默认值 | = defaultValue | this.color = Colors.blue |
| 只读保证 | 约定不修改 | final 编译期强制报错 |
Flutter 没有
props对象,构造函数参数就是 props,本质思想完全一致——数据从父流向子,子不能修改。
无状态子组件(StatelessWidget)
// 子组件
class MyCard extends StatelessWidget {
final String title; // 必传 [required]
final String? subtitle; // 可选,默认 null
final Color color; // 有默认值
const MyCard({
Key? key,
required this.title,
this.subtitle,
this.color = Colors.blue,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
color: color,
padding: EdgeInsets.all(16),
child: Column(
children: [
Text(title, style: TextStyle(color: Colors.white, fontSize: 20)),
if (subtitle != null) Text(subtitle!), // 可选参数需判空
],
),
);
}
}
// 父组件使用
Column(
children: [
MyCard(title: '标题A'), // 只传必填
MyCard(title: '标题B', subtitle: '副标题'), // 传可选
MyCard(title: '标题C', color: Colors.red), // 覆盖默认值
],
)
父 setState 驱动子组件更新
// 子组件(无状态,只展示)
class CounterDisplay extends StatelessWidget {
final int count;
final String label;
const CounterDisplay({Key? key, required this.count, required this.label}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text('$label: $count', style: TextStyle(fontSize: 24));
}
}
// 父组件(有状态,持有数据)
class _ParentPageState extends State<ParentPage> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
CounterDisplay(count: _count, label: '当前数量'), // 父的 state 传给子
ElevatedButton(
onPressed: () => setState(() => _count++), // 父 setState → 子重建
child: Text('点击 +1'),
),
],
);
}
}
💡 数据为什么放在父组件? 如果
_count放在子组件,只有子组件自己能修改和感知它。多个子组件共享同一份数据时,数据必须提升到共同的父组件——这叫状态提升,和 React 的 lifting state up 完全一致。
子传父(回调函数)
子传父 = 父把函数传给子 + 子调用该函数,和 React 的 onChange/onClick 传函数一致。
常见函数类型
final VoidCallback onTap; // 无参无返回,如按钮点击通知父
final Function(String) onChanged; // 简写,传一个 String
final void Function(String) onChanged; // 完整写法
final ValueChanged<String> onChanged; // 官方推荐写法,等同 Function(String)
案例:菜单删除(子通知父删除数据)
// 父组件 — 持有数据,传删除函数给子
class _ParentPageState extends State<ParentPage> {
List<String> _list = ['鱼香肉丝', '宫保鸡丁', '麻婆豆腐', '京酱肉丝', '溜肉片'];
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.all(10),
children: List.generate(_list.length, (index) {
return Child(
foodName: _list[index],
index: index,
delFood: (int i) {
setState(() {
_list.removeAt(i); // 数据修改包在 setState 内
});
},
);
}),
);
}
}
// 子组件 — 无状态,只展示 + 触发回调
class Child extends StatelessWidget {
final String foodName;
final int index;
final Function(int index) delFood; // 接收父传来的函数
const Child({
super.key, // Dart 2.17+ 简写
required this.foodName,
required this.index,
required this.delFood,
});
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.topRight,
children: [
Container(
color: Colors.blue,
alignment: Alignment.center,
child: Text(foodName, style: TextStyle(color: Colors.white, fontSize: 20)),
),
IconButton(
color: Colors.red,
onPressed: () => delFood(index), // 调用父传来的函数,把 index 传回去
icon: Icon(Icons.delete),
),
],
);
}
}
面试点:
setState应包裹数据修改:setState(() { _list.removeAt(i); }),而非修改完再调空的setState(() {})super.key是 Dart 2.17 语法糖,老写法是Key? key+: super(key: key)
InheritedWidget — 跨层级数据共享
不需要一层层 constructor 透传,后代任意层级都能直接读取。
// 1. 定义 InheritedWidget
class MyData extends InheritedWidget {
final int count;
const MyData({super.key, required this.count, required super.child});
// 后代通过 of(context) 获取
static MyData of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyData>()!;
}
@override
bool updateShouldNotify(MyData old) => count != old.count; // 数据变了才通知后代重建
}
// 2. 祖先注入
MyData(count: 42, child: MyApp())
// 3. 任意后代读取
final count = MyData.of(context).count;
💡 Tip: 实际开发很少手写
InheritedWidget,直接用对它的封装Provider。 面试点:updateShouldNotify返回true时所有依赖该数据的后代会重建,false不重建——性能优化的关键。
Provider — 官方推荐状态管理
解决了构造函数需要一层层透传的问题,任意层级的组件都能读写共享数据。
# pubspec.yaml
dependencies:
provider: ^6.0.0
// 1. 定义数据模型,继承 ChangeNotifier
class CounterModel extends ChangeNotifier {
int count = 0;
void increment() {
count++;
notifyListeners(); // 通知所有监听者重建,等同 setState
}
}
// 2. 在顶层注入
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}
// 3. 任意后代读取数据(会监听变化)
Consumer<CounterModel>(
builder: (context, model, child) {
return Text('${model.count}');
},
)
// 4. 任意后代修改数据
ElevatedButton(
onPressed: () => context.read<CounterModel>().increment(),
child: Text('+1'),
)
context.watch vs context.read:
| 方法 | 用途 | 使用位置 |
|---|---|---|
context.watch<T>() | 监听数据变化,数据更新时当前 Widget 重建 | build 方法里 |
context.read<T>() | 只读取一次,不监听变化 | 事件回调里 |
⚠️ 在
onPressed里用watch会报错,因为回调不在 build 阶段。面试点: Provider 底层是
InheritedWidget,notifyListeners触发的重建只影响调用了watch/Consumer的组件,不会重建整个树,性能比setState更精准。
EventBus — 全局事件通信
适合完全不相关的组件(如底部 tab 切换通知深层页面刷新),双方不需要知道对方的存在。
# pubspec.yaml
dependencies:
event_bus: ^2.0.0
// 1. 定义事件
class RefreshEvent {
final String message;
RefreshEvent(this.message);
}
// 2. 创建全局 EventBus 实例(通常放在单独文件)
final eventBus = EventBus();
// 3. 订阅方(在 initState 里监听,在 dispose 里取消)
class _PageAState extends State<PageA> {
late StreamSubscription _sub;
@override
void initState() {
super.initState();
_sub = eventBus.on<RefreshEvent>().listen((event) {
print('收到事件:${event.message}');
setState(() {});
});
}
@override
void dispose() {
_sub.cancel(); // 必须取消,否则内存泄漏!
super.dispose();
}
}
// 4. 发布方(任意位置)
eventBus.fire(RefreshEvent('需要刷新了'));
⚠️ 必须在
dispose里调用_sub.cancel(),否则页面销毁后订阅依然存活,造成内存泄漏。💡 为什么不所有场景都用 EventBus? EventBus 是全局的,事件发出后所有订阅者都会收到,数据流向难以追踪,调试困难。父子关系明确时优先用构造函数/回调/Provider。
Bloc — 大型项目状态管理
严格的事件驱动架构,数据流向清晰:
UI ──发送 Event──▶ Bloc ──输出 State──▶ UI 重建
// 1. 定义事件和状态
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
// 2. 定义 Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<IncrementEvent>((event, emit) => emit(state + 1));
}
}
// 3. 注入
BlocProvider(
create: (_) => CounterBloc(),
child: MyApp(),
)
// 4. 读取状态
BlocBuilder<CounterBloc, int>(
builder: (context, count) => Text('$count'),
)
// 5. 发送事件
context.read<CounterBloc>().add(IncrementEvent());
💡 什么时候选 Bloc 而不是 Provider? Provider 适合中小项目,逻辑简单直接写在 Model 里。Bloc 强制把 UI、事件、状态分离,适合多人协作的大型项目,代码可测试性更强。 面试点: Bloc 每个操作都有明确的 Event 类型,相当于操作日志,调试时能清楚看到"谁触发了什么导致状态变成了什么",这是 Provider 做不到的。
子组件选型总结
| 子组件场景 | 用哪种 |
|---|---|
| 纯展示(卡片、文字、图标) | StatelessWidget |
| 有内部交互(输入框、动画) | StatefulWidget |
| 数据来自父且会变化 | 子 StatelessWidget + 父 setState |
| 跨层级共享简单数据 | Provider |
| 完全不相关组件间的全局事件 | EventBus |
| 大型项目、多人协作、需要可测性 | Bloc / Riverpod |