Flutter 教学笔记

7 阅读12分钟

Flutter 教学笔记

最近准备使用使用开发APP项目,为了方便记录,通过B站某教学视频和Claude简单整理了一份Dart语法资料。这里也给大家分享一下!!!

目录

  1. Material 基础
    • MaterialApp · Scaffold
  2. 组件基础
    • StatelessWidget · StatefulWidget · 生命周期
  3. 事件处理
    • GestureDetector · 计数器 Demo
  4. 布局组件
    • Container · Center · Align · Padding
    • Column · Row · Flex · Expanded · Flexible
    • Wrap · Stack · Positioned
  5. 常用组件
    • Text · TextField · Image
  6. 滚动组件
    • SingleChildScrollView · ListView · GridView · CustomScrollView · PageView
  7. 组件通信
    • 父传子 · 子传父 · 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();
  }
}

生命周期

完整执行顺序:

顺序方法执行次数调用时机适合做的事
1createState()一次Widget 初始化时创建 State 对象
2initState()一次State 插入树后立刻初始化变量、创建 Controller、订阅 Stream、首次网络请求
3didChangeDependencies()多次initState 后 + InheritedWidget 变化时读取 Theme.of(context)、MediaQuery.of(context)
4build()多次初始化或状态更新后只做 UI 构建,不放耗时操作,不调用 setState()
5setState()多次主动标记状态变更更新计数器、切换选中状态、修改列表数据
6didUpdateWidget()多次父组件传入新配置时对比新旧 widget 属性差异、重置 Controller、重新发起请求
7reassemble()多次调试 Hot Reload 时仅调试用,生产不执行
8deactivate()多次State 从树中暂时移除时暂停动画、保存临时草稿
9dispose()一次State 永久移除时释放 Controller、取消 Stream 订阅、取消网络请求、清理定时器
        🟢 组件创建
             │
    ① createState()
             │
    ② initState()
             │
    ③ didChangeDependencies() ◄── InheritedWidget 更新时重复触发
             │
    ④ build() ◄──────────────────────────────────────────┐
             │                                            │
      ┌──────┼──────────────────────┐                    │
      ▼      ▼                      ▼                    │
⑤ setState()  ⑥ didUpdateWidget()  ⑦ reassemble()        │
      └──────┴──────────────────────┘ ──触发重建──────────┘
             │
      组件被移除
             │
    ⑧ deactivate()(暂时移除,可能重新插入)
             │
    ⑨ dispose()(永久移除,释放资源)
             │
        🔴 组件销毁

核心规律: 凡是能改变状态或配置的操作,最终都会回到 build(),这是 Flutter UI 更新的唯一入口。

InheritedWidget: 专门用于在 Widget 树中自顶向下高效共享数据。Flutter 内置的 ThemeMediaQuery 底层都基于它实现。


三、事件处理

GestureDetector

Flutter 中最常用、功能最丰富的手势检测组件,通过包裹目标组件添加交互。

GestureDetector(
  onTap: () => print('点击'),
  onDoubleTap: () => print('双击'),
  onLongPress: () => print('长按'),
  child: Text('可点击区域'),
)

其他常用交互组件:

组件类别核心组件使用场景
专用按钮组件ElevatedButtonTextButtonOutlinedButton内置点击动画和样式
视觉反馈组件InkWellMaterial 水纹扩散效果
其他交互组件IconButtonSwitchCheckbox具有特定功能的交互控件

计数器 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 提供了丰富的布局组件:

组件类别核心组件主要特点
基础容器ContainerCenterAlignPaddingSizedBox装饰、对齐、边距、固定尺寸
线性布局RowColumn水平或垂直线性排列
弹性布局FlexExpandedFlexible按比例分配剩余空间
层叠布局StackPositioned子组件重叠堆叠
流式布局Wrap主轴空间不足时自动换行
滚动布局ListViewGridViewSingleChildScrollView可滚动列表、网格或整页视图
约束布局SafeAreaLayoutBuilder避开系统 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 / decorationDecoratedBox背景色、圆角、边框
paddingPadding内边距
alignmentAlign子组件对齐
width / heightSizedBox固定尺寸
constraintsConstrainedBox最大/最小尺寸约束
transformTransform矩阵变换

实际建议: 只需要单一功能时优先用底层 Widget(只需间距用 Padding,只需固定尺寸用 SizedBox),性能更好也更语义化。需要组合多个样式时再用 Container


Center

将子组件在父容器内水平和垂直居中

  • Center 本身不能设置宽高,其大小取决于父组件约束
  • 等价于 Align(alignment: Alignment.center, ...)
Center(
  child: Container(
    color: Colors.blue,
    child: Text('居中'),
  ),
)

Align

精确控制子组件在父容器内的对齐位置。

属性说明
alignment对齐方式,如 Alignment.bottomRight
widthFactorAlign 宽度 = 子组件宽度 × 该因子
heightFactorAlign 高度 = 子组件高度 × 该因子
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 等价说明
mainAxisAlignmentjustify-content主轴(垂直)对齐:start/end/center/spaceBetween/spaceAround/spaceEvenly
crossAxisAlignmentalign-items交叉轴(水平)对齐:start/end/center/stretch
mainAxisSizeheightmax=占满父级(默认),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 本身不支持滚动,内容超出时用 SingleChildScrollViewListView 包裹
  • 子组件总高度超出会报 overflow 错误,用 Expanded 或滚动组件解决

Row

水平排列子组件,等价于 CSS display: flex; flex-direction: row

属性CSS 等价说明
mainAxisAlignmentjustify-content主轴(水平)对齐:start/end/center/spaceBetween/spaceAround/spaceEvenly
crossAxisAlignmentalign-items交叉轴(垂直)对齐:start/end/center/stretch
mainAxisSizewidthmax=占满父级(默认),min=包裹内容
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [Text('左'), Text('右')],
)

注意: 子组件总宽度超过 Row 宽度会报 overflow 错误,用 Expanded 让某个子组件自适应解决。


Flex / Expanded / Flexible

FlexRowColumn 的底层实现,通过 direction 控制方向。实际开发中直接用 Row / ColumnFlex 用在需要动态切换方向时。

Expanded vs Flexible:

ExpandedFlexible
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 等价说明
spacingcolumn-gap主轴方向子组件间距
runSpacingrow-gap行间距
alignmentjustify-content主轴方向对齐(WrapAlignment
runAlignmentalign-content多行整体在交叉轴的对齐
directionflex-directionAxis.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.centertext-align: center
maxLines: 2-
overflow: TextOverflow.ellipsistext-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 等价说明
controllerref + valueTextEditingController,读取/设置/监听
decorationCSS 样式输入框外观(边框、提示文字、前后图标)
hintText(在 decoration 里)placeholder占位提示
labelText(在 decoration 里)<label>浮动标签
obscureText: truetype="password"密码遮罩
keyboardTypetype="number"弹出键盘类型
maxLinesrows最大行数(>1 变为多行文本框)
maxLengthmaxlength最大字符数,自动显示计数器
enabled: falsedisabled禁用
readOnly: truereadonly只读
onChangedonChange输入变化回调
inputFormattersoninput 过滤限制输入内容(如只允许数字)

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 + Form
  • maxLines: null 表示无限行数(自动增高)
  • controller 必须在 dispose() 中调用 .dispose()

Image

显示图片,支持网络图、本地资源图、内存图等多种来源。

构造方式说明
Image.network(url)网络图片
Image.asset('...')本地资源图(需在 pubspec.yaml 注册)
Image.file(File(...))本地文件图
Image.memory(bytes)内存中的图片数据
属性CSS 等价说明
fit: BoxFit.coverobject-fit: cover裁剪填满,最常用
fit: BoxFit.containobject-fit: contain保持比例完全显示
fit: BoxFit.fillobject-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.yamlflutter.assets 中注册
  • 生产环境推荐 cached_network_image 包,自动缓存避免重复下载
  • loadingBuildererrorBuilder 是生产环境必须处理的两个状态

六、滚动组件

SingleChildScrollView

为单个子 Widget 提供可滚动容器,适合页面整体滚动,不适合大量数据列表。

属性说明
scrollDirectionAxis.vertical(默认)/ Axis.horizontal
controllerScrollController,编程控制滚动位置
physicsBouncingScrollPhysics()(回弹)/ 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

可滚动列表,支持懒加载,是处理列表数据的核心组件。

三种变体对比

场景用哪个
固定少量 itemListView
大量/动态数据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

共有属性

属性归属效果
crossAxisCountcount 专属固定每行列数
maxCrossAxisExtentextent 专属每项最大宽度,列数自动算
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 适配的常用手段。extentbuilder 不互斥,buildergridDelegate 可用 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 countextentchildren 列表意味着所有 item 进入页面时全部创建,内存瞬间暴涨必然卡顿甚至崩溃。 面试点: GridView.builderListView.builder 都基于 SliverMultiBoxAdaptorElement 实现元素复用,原理和 Android RecyclerView、iOS UICollectionView 的 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)统一在一个滚动容器中。

为什么需要它?

ListViewGridView 无法混合在同一个滚动区域里。

💡 为什么不在 Column 里嵌套 ListView? Column 里嵌套 ListView 会报 "unbounded height" 错误,因为 ListView 需要固定高度的父容器。即使加了 shrinkWrap: true,会一次性渲染所有子项,失去懒加载优势。 面试点: 面试官常问"如何在列表顶部加一个 Banner",错误答案是 Column + ListView,正确答案是 CustomScrollView + SliverToBoxAdapter + SliverList

Sliver 是什么?

Sliver 是能感知滚动位置的特殊 Widget,只能用在 CustomScrollViewslivers 里。

普通 Widget(Container、Text)  →  用 SliverToBoxAdapter 包裹后放入
滚动类 Widget(ListView、Grid) →  用对应的 Sliver 版本替换

普通 Widget vs Sliver 版本对照

普通写法Sliver 版本说明
ListView.builderSliverList.builder懒加载列表
ListView.separatedSliverList.separated带分隔线列表
GridView.countSliverGrid.count网格
任意普通 WidgetSliverToBoxAdapter包裹普通 Widget
吸顶 HeaderSliverPersistentHeader需配合 delegate
可折叠 AppBarSliverAppBar内置折叠动效

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
需要吸顶 HeaderCustomScrollView + SliverPersistentHeader
需要可折叠头图CustomScrollView + SliverAppBar

PageView

页面级整屏滑动组件,适合轮播图或 App 引导页,每次滑动切换一整屏内容。

属性说明
controllerPageController,控制页面跳转和读取当前页
onPageChanged页面切换回调,参数为新页面索引
itemCount总页数(.builder 版)
scrollDirectionAxis.horizontal(默认)/ Axis.vertical 上下翻页
physicsBouncingScrollPhysics() 回弹 / NeverScrollableScrollPhysics()
pageSnappingtrue(默认)每次滑动整页吸附

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 的对比

对比项ReactFlutter
传参方式JSX 属性 <C title="x" />构造函数具名参数 C(title: 'x')
接收方式props 对象或解构类的成员变量(final
必传校验PropTypes / TypeScriptrequired 关键字
默认值= defaultValuethis.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 底层是 InheritedWidgetnotifyListeners 触发的重建只影响调用了 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

參考

www.bilibili.com/video/BV1wR…