Flutter 入门

最后更新:2026-03-05 前置知识:Dart入门(/dart-ru-men/) 1. Flutter 是什么(#flutter-%E6%98%AF%E4%BB%80%E4%B9%88) 2. 安装与环境配置(#%E5%AE%89%E8%A3%85%E4%B8%8E%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE) 3. 项目结构(#%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84) 4. 热重载与热重启(#%E7%83%AD%E9%87%8D%E8%BD%BD%E4%B8%8E%E7%83%AD%E9

分享

最后更新:2026-03-05

官方文档:https://docs.flutter.dev/
适用版本:Flutter 3.x(2026-05-07 核实)

前置知识:Dart入门


目录

  1. Flutter 是什么
  2. 安装与环境配置
  3. 项目结构
  4. 热重载与热重启
  5. Widget 核心概念
  6. 常用基础 Widget
  7. 状态管理
  8. 导航与路由
  9. 网络请求
  10. 本地存储
  11. 常用 Widget 进阶
  12. 主题与样式
  13. 综合实战:新闻阅读 App

Flutter 是什么

Flutter 是 Google 开发的开源 UI 框架,使用 Dart 语言编写,可从单一代码库编译到 Android、iOS、Web、Windows、macOS、Linux 六个平台。

跨平台原理

Flutter 不依赖平台原生控件,而是自带一套渲染引擎(Skia / Impeller),直接在画布上绘制每一个像素。这意味着:

  • 所有平台上的 UI 表现完全一致
  • 不受原生控件版本差异影响
  • 渲染性能接近原生(目标 60/120 fps)

与 React Native 的区别

对比项 Flutter React Native
语言 Dart JavaScript / TypeScript
渲染方式 自绘引擎,不用原生控件 调用原生控件(Bridge / JSI)
UI 一致性 全平台像素级一致 依赖平台,有差异
性能 高,无 Bridge 开销 JSI 后有所提升,但仍有开销
生态 较新,成长快 较成熟,包数量多
学习曲线 需学 Dart 前端开发者上手快

安装与环境配置

Windows

  1. 下载 Flutter SDK:https://docs.flutter.dev/get-started/install/windows
  2. 解压到无空格、无中文路径,例如 C:\dev\flutter
  3. C:\dev\flutter\bin 加入系统 PATH
  4. 安装 Android Studio,打开 SDK Manager 安装 Android SDK
  5. 运行 flutter doctor 检查环境
# 检查环境
flutter doctor

# 接受 Android 许可证
flutter doctor --android-licenses

macOS

# 使用 Homebrew 安装(推荐)
brew install --cask flutter

# 或手动下载解压后添加到 PATH
export PATH="$HOME/dev/flutter/bin:$PATH"

# iOS 需要 Xcode
xcode-select --install
sudo xcodebuild -license accept

# 检查环境
flutter doctor

flutter doctor 输出示例

Doctor summary (to see all details, run flutter doctor -v):
[v] Flutter (Channel stable, 3.19.0, on macOS 14.0)
[v] Android toolchain - develop for Android devices
[v] Xcode - develop for iOS and macOS
[v] Chrome - develop for the web
[v] Android Studio (version 2023.1)
[v] VS Code (version 1.85.0)
[v] Connected device (3 available)
[v] Network resources

每一项显示 [v] 表示正常,[!] 表示有警告,[x] 表示有问题需要修复。


项目结构

# 创建新项目
flutter create my_app
cd my_app
flutter run
my_app/
├── android/          # Android 原生工程,一般不需要手动修改
├── ios/              # iOS 原生工程,一般不需要手动修改
├── lib/              # Dart 代码,主要工作区
│   └── main.dart     # 应用入口
├── test/             # 单元测试
├── web/              # Web 平台配置
├── pubspec.yaml      # 项目配置,依赖声明
└── pubspec.lock      # 依赖版本锁定文件

lib/main.dart

import 'package:flutter/material.dart';

// 应用入口函数
void main() {
  runApp(const MyApp());
}

// 根 Widget,通常是 StatelessWidget
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('首页')),
      body: const Center(child: Text('Hello Flutter')),
    );
  }
}

pubspec.yaml

name: my_app
description: 我的第一个 Flutter 项目
version: 1.0.0+1

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  # 添加第三方包
  provider: ^6.1.1
  http: ^1.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0

flutter:
  uses-material-design: true
  # 声明本地图片资源
  assets:
    - assets/images/
  # 声明本地字体
  fonts:
    - family: MyFont
      fonts:
        - asset: assets/fonts/MyFont-Regular.ttf

添加依赖后运行 flutter pub get 安装。


热重载与热重启

操作 快捷键(VS Code) 效果
热重载(Hot Reload) r(终端)/ Ctrl+F5 注入新代码,保留应用状态,速度极快
热重启(Hot Restart) R(终端)/ Ctrl+Shift+F5 重启 Dart VM,清空状态,比冷启动快
冷启动 重新运行 完整编译运行

热重载的限制:

  • 修改 initStatedispose 等生命周期方法后需要热重启
  • 修改 main() 函数后需要热重启
  • 修改原生代码(android/ios 目录)后需要冷启动

Widget 核心概念

一切皆 Widget

Flutter 中的一切都是 Widget:文字、按钮、布局容器、内边距、动画、手势检测器,甚至主题、路由配置也是 Widget。

// 就连 Padding 和 Center 也是 Widget
Center(
  child: Padding(
    padding: const EdgeInsets.all(16.0),
    child: Text('Hello'),
  ),
)

StatelessWidget vs StatefulWidget

对比项 StatelessWidget StatefulWidget
状态 无内部状态 有内部状态(State 对象)
重建时机 父 Widget 重建时 父重建 或 调用 setState() 时
使用场景 纯展示,数据来自外部 需要响应用户交互、维护内部数据
性能 稍好 稍差,但影响可忽略
// StatelessWidget 示例
class GreetingCard extends StatelessWidget {
  final String name; // 数据从外部传入

  const GreetingCard({super.key, required this.name});

  @override
  Widget build(BuildContext context) {
    return Text('你好,$name');
  }
}
// StatefulWidget 示例
class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0; // 内部状态

  void _increment() {
    // setState 触发 build 方法重新执行
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('计数:$_count'),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('加一'),
        ),
      ],
    );
  }
}

Widget 树、Element 树、RenderObject 树

Flutter 内部维护三棵树:

  • Widget 树:开发者描述 UI 的配置信息,不可变对象,每次 build 都可能重建
  • Element 树:Widget 树的实例化,维护 Widget 与渲染对象之间的对应关系,生命周期比 Widget 长
  • RenderObject 树:负责实际的布局计算和绘制

开发者只需关心 Widget 树,Flutter 框架管理另外两棵树,这也是 Flutter 能做到高效更新的原因——当 Widget 重建时,Element 树会复用旧的 Element,只更新变化的部分。

build 方法与 BuildContext

@override
Widget build(BuildContext context) {
  // context 代表当前 Widget 在 Widget 树中的位置
  // 通过 context 可以向上查找祖先 Widget 的数据
  final theme = Theme.of(context);        // 获取主题
  final mediaQuery = MediaQuery.of(context); // 获取屏幕信息
  final navigator = Navigator.of(context);  // 获取导航器

  return Text(
    '屏幕宽度:${mediaQuery.size.width}',
    style: theme.textTheme.bodyLarge,
  );
}

注意事项:

  • build 方法应该是纯函数,不应有副作用
  • 不要在 build 中做耗时操作
  • build 可能被频繁调用,应保持轻量

常用基础 Widget

Container

Container 是最常用的布局 Widget,类似 CSS 中的 div,可设置大小、内外边距、背景、圆角等。

参数表

参数 类型 默认值 说明
width double? - 宽度,不设则由子 Widget 决定
height double? - 高度
margin EdgeInsetsGeometry? - 外边距
padding EdgeInsetsGeometry? - 内边距
alignment AlignmentGeometry? - 子 Widget 对齐方式
decoration Decoration? - 背景、边框、圆角、阴影等
color Color? - 背景色(与 decoration 互斥)
child Widget? - 子 Widget
constraints BoxConstraints? - 尺寸约束
transform Matrix4? - 变换矩阵
Container(
  width: 200,
  height: 100,
  margin: const EdgeInsets.all(16),       // 四周 16 像素外边距
  padding: const EdgeInsets.symmetric(    // 水平 12,垂直 8 内边距
    horizontal: 12,
    vertical: 8,
  ),
  alignment: Alignment.center,            // 子 Widget 居中
  decoration: BoxDecoration(
    color: Colors.blue.shade100,          // 背景色
    borderRadius: BorderRadius.circular(12), // 圆角
    border: Border.all(                   // 边框
      color: Colors.blue,
      width: 2,
    ),
    boxShadow: [                          // 阴影
      BoxShadow(
        color: Colors.black26,
        blurRadius: 8,
        offset: const Offset(0, 4),
      ),
    ],
  ),
  child: const Text('我是 Container'),
)

Row / Column

Row 水平排列子 Widget,Column 垂直排列。

参数表

参数 类型 默认值 说明
children List<Widget> [] 子 Widget 列表
mainAxisAlignment MainAxisAlignment .start 主轴对齐(Row:水平,Column:垂直)
crossAxisAlignment CrossAxisAlignment .center 交叉轴对齐
mainAxisSize MainAxisSize .max 主轴占用空间:.max 尽量大,.min 包裹内容
verticalDirection VerticalDirection .down Column 排列方向

MainAxisAlignment 取值

取值 效果
start 起始端对齐
end 末端对齐
center 居中
spaceBetween 两端对齐,子项间均匀留白
spaceAround 子项两侧留白相等,边缘留白是中间的一半
spaceEvenly 所有空白均匀分配
// Row 示例
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    const Icon(Icons.home),
    const Text('标题'),
    IconButton(
      onPressed: () {},
      icon: const Icon(Icons.settings),
    ),
  ],
)

// Column 示例
Column(
  mainAxisSize: MainAxisSize.min,      // 高度包裹内容
  crossAxisAlignment: CrossAxisAlignment.start, // 左对齐
  children: const [
    Text('第一行'),
    SizedBox(height: 8),               // 间距
    Text('第二行'),
    SizedBox(height: 8),
    Text('第三行'),
  ],
)

Stack / Positioned

Stack 将子 Widget 叠加显示,类似 CSS 中的 position: absolute

Stack 参数表

参数 类型 默认值 说明
children List<Widget> [] 子 Widget,后面的显示在上层
alignment AlignmentGeometry topStart 未定位子 Widget 的对齐方式
fit StackFit loose 未定位子 Widget 的约束方式
clipBehavior Clip hardEdge 溢出裁剪方式

Positioned 参数表

参数 类型 默认值 说明
child Widget 必填 子 Widget
top double? - 距顶部距离
bottom double? - 距底部距离
left double? - 距左侧距离
right double? - 距右侧距离
width double? - 宽度
height double? - 高度
Stack(
  alignment: Alignment.center,
  children: [
    // 底层:背景图
    Container(
      width: 300,
      height: 200,
      color: Colors.grey.shade200,
    ),
    // 右上角:徽章
    Positioned(
      top: 8,
      right: 8,
      child: Container(
        padding: const EdgeInsets.all(4),
        decoration: const BoxDecoration(
          color: Colors.red,
          shape: BoxShape.circle,
        ),
        child: const Text(
          '99',
          style: TextStyle(color: Colors.white, fontSize: 12),
        ),
      ),
    ),
    // 底部居中:文字说明
    const Positioned(
      bottom: 16,
      left: 0,
      right: 0,
      child: Text(
        '图片说明',
        textAlign: TextAlign.center,
        style: TextStyle(color: Colors.black54),
      ),
    ),
  ],
)

Expanded / Flexible

在 Row/Column 中按比例分配剩余空间。

参数表

参数 类型 默认值 说明
flex int 1 权重,相对于同级 Expanded/Flexible 的比例
fit(仅 Flexible) FlexFit loose tight:强制填满,loose:最多占用 flex 比例空间
child Widget 必填 子 Widget
Row(
  children: [
    // 占剩余空间的 2/3
    Expanded(
      flex: 2,
      child: Container(color: Colors.blue, height: 50),
    ),
    // 占剩余空间的 1/3
    Expanded(
      flex: 1,
      child: Container(color: Colors.red, height: 50),
    ),
  ],
)

// Flexible 示例:文字不超过可用空间
Row(
  children: [
    Flexible(
      child: Text(
        '这是一段很长很长很长很长很长很长的文字,不会溢出',
        overflow: TextOverflow.ellipsis, // 超出显示省略号
      ),
    ),
    const Icon(Icons.arrow_forward),
  ],
)

SizedBox

固定尺寸的盒子,常用于添加间距或约束子 Widget 大小。

// 固定尺寸
const SizedBox(width: 200, height: 100, child: Text('固定大小'))

// 常用作间距(比 Container 轻量)
const Column(
  children: [
    Text('第一行'),
    SizedBox(height: 16),  // 垂直间距
    Text('第二行'),
  ],
)

// 强制子 Widget 充满父容器
SizedBox.expand(child: Container(color: Colors.blue))

Padding / Center / Align

// Padding:添加内边距
Padding(
  padding: const EdgeInsets.only(left: 16, top: 8), // 仅左和上
  child: const Text('有内边距的文字'),
)

// Center:居中
const Center(child: Text('居中'))

// Align:指定对齐位置
Align(
  alignment: Alignment.bottomRight, // 右下角
  child: const Text('右下角'),
)
// Alignment 常用值:topLeft, topCenter, topRight,
//   centerLeft, center, centerRight,
//   bottomLeft, bottomCenter, bottomRight
// 也可以用坐标:Alignment(0.0, 0.0) 表示正中心

Wrap

类似 Row/Column,但子项超出时自动换行/换列。

参数表

参数 类型 默认值 说明
direction Axis horizontal 排列方向
spacing double 0.0 主轴方向的子项间距
runSpacing double 0.0 交叉轴方向的行间距
alignment WrapAlignment start 主轴对齐
runAlignment WrapAlignment start 多行时的行对齐
children List<Widget> [] 子 Widget
// 标签流式布局
Wrap(
  spacing: 8,      // 横向间距
  runSpacing: 8,   // 纵向间距
  children: ['Flutter', 'Dart', 'Android', 'iOS', 'Web', 'Windows']
      .map(
        (tag) => Chip(
          label: Text(tag),
          backgroundColor: Colors.blue.shade100,
        ),
      )
      .toList(),
)

Text

参数表

参数 类型 默认值 说明
data String 必填 文字内容
style TextStyle? - 文字样式
textAlign TextAlign? - 水平对齐
maxLines int? - 最大行数
overflow TextOverflow? - 溢出处理
softWrap bool? true 是否自动换行
textScaler TextScaler? - 文字缩放

TextStyle 参数表

参数 类型 说明
fontSize double? 字体大小
fontWeight FontWeight? 字重:w100~w900bold = w700
fontStyle FontStyle? normal / italic
color Color? 文字颜色
backgroundColor Color? 背景色
letterSpacing double? 字间距
wordSpacing double? 词间距
height double? 行高(相对于 fontSize 的倍数)
decoration TextDecoration? 装饰线:underline/overline/lineThrough
decorationColor Color? 装饰线颜色
fontFamily String? 字体族
shadows List<Shadow>? 文字阴影
Text(
  '这是标题文字',
  style: const TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.black87,
    letterSpacing: 1.5,
    height: 1.4,
    decoration: TextDecoration.none,
  ),
  textAlign: TextAlign.center,
  maxLines: 2,
  overflow: TextOverflow.ellipsis,
)

RichText / TextSpan

在同一段文字中混合多种样式。

RichText(
  text: TextSpan(
    // 默认样式,子 Span 会继承
    style: const TextStyle(fontSize: 16, color: Colors.black),
    children: [
      const TextSpan(text: '普通文字,'),
      TextSpan(
        text: '蓝色加粗,',
        style: const TextStyle(
          color: Colors.blue,
          fontWeight: FontWeight.bold,
        ),
      ),
      TextSpan(
        text: '可点击链接',
        style: const TextStyle(
          color: Colors.blue,
          decoration: TextDecoration.underline,
        ),
        recognizer: TapGestureRecognizer()
          ..onTap = () {
            // 点击回调(需要 import 'package:flutter/gestures.dart')
            print('链接被点击');
          },
      ),
    ],
  ),
)

Image

参数表

参数 类型 默认值 说明
fit BoxFit? - 图片适应方式
width double? - 宽度
height double? - 高度
color Color? - 混合颜色(配合 colorBlendMode 使用)
alignment Alignment center 图片对齐方式
repeat ImageRepeat noRepeat 平铺方式
errorBuilder Function? - 加载失败时显示的 Widget
loadingBuilder Function? - 加载中显示的 Widget(仅 network)

BoxFit 取值

取值 效果
fill 拉伸填满,不保持比例
contain 保持比例,显示完整图片
cover 保持比例,覆盖整个区域,可能裁剪
fitWidth 宽度适应,高度可能裁剪
fitHeight 高度适应,宽度可能裁剪
none 原始大小,不缩放
scaleDown 仅缩小,不放大
// 本地资源图片(需在 pubspec.yaml 的 assets 中声明)
Image.asset(
  'assets/images/logo.png',
  width: 100,
  height: 100,
  fit: BoxFit.contain,
)

// 网络图片
Image.network(
  'https://example.com/photo.jpg',
  width: 300,
  height: 200,
  fit: BoxFit.cover,
  // 加载中显示进度
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;
    return Center(
      child: CircularProgressIndicator(
        value: loadingProgress.expectedTotalBytes != null
            ? loadingProgress.cumulativeBytesLoaded /
                loadingProgress.expectedTotalBytes!
            : null,
      ),
    );
  },
  // 加载失败显示占位
  errorBuilder: (context, error, stackTrace) {
    return const Icon(Icons.broken_image, size: 100);
  },
)

Icon

// 使用 Material Icons
const Icon(Icons.home, size: 32, color: Colors.blue)
const Icon(Icons.favorite, size: 24, color: Colors.red)
const Icon(Icons.settings)

// 常用图标
// Icons.home, Icons.search, Icons.person, Icons.settings
// Icons.add, Icons.delete, Icons.edit, Icons.close
// Icons.arrow_back, Icons.arrow_forward
// Icons.favorite, Icons.favorite_border
// Icons.share, Icons.download, Icons.upload
// Icons.notification_add, Icons.notifications
// Icons.menu, Icons.more_vert(竖三点), Icons.more_horiz(横三点)

ElevatedButton / TextButton / OutlinedButton

参数表(三种按钮通用)

参数 类型 默认值 说明
onPressed VoidCallback? - 点击回调,为 null 时按钮禁用
onLongPress VoidCallback? - 长按回调
child Widget 必填 按钮内容
style ButtonStyle? - 样式配置
autofocus bool false 是否自动获取焦点

ButtonStyle 常用参数

参数 类型 说明
backgroundColor MaterialStateProperty<Color?> 背景色
foregroundColor MaterialStateProperty<Color?> 前景色(文字、图标)
padding MaterialStateProperty<EdgeInsetsGeometry?> 内边距
shape MaterialStateProperty<OutlinedBorder?> 形状
elevation MaterialStateProperty<double?> 海拔阴影
minimumSize MaterialStateProperty<Size?> 最小尺寸
fixedSize MaterialStateProperty<Size?> 固定尺寸
// ElevatedButton:有背景色的实心按钮
ElevatedButton(
  onPressed: () => print('点击'),
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,        // 背景色
    foregroundColor: Colors.white,       // 文字颜色
    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(8),
    ),
    elevation: 4,
    minimumSize: const Size(120, 48),
  ),
  child: const Text('确认'),
)

// TextButton:无背景的文字按钮
TextButton(
  onPressed: () => print('取消'),
  child: const Text('取消'),
)

// OutlinedButton:有边框的按钮
OutlinedButton(
  onPressed: () {},
  style: OutlinedButton.styleFrom(
    side: const BorderSide(color: Colors.blue, width: 2),
  ),
  child: const Text('次要操作'),
)

// IconButton:图标按钮
IconButton(
  onPressed: () {},
  icon: const Icon(Icons.favorite),
  color: Colors.red,
  iconSize: 32,
  tooltip: '收藏',     // 长按显示的提示文字
)

// FloatingActionButton:悬浮按钮
FloatingActionButton(
  onPressed: () {},
  tooltip: '添加',
  child: const Icon(Icons.add),
)

// 禁用按钮:将 onPressed 设为 null
ElevatedButton(
  onPressed: null,  // 禁用
  child: const Text('已禁用'),
)

TextField

参数表

参数 类型 默认值 说明
controller TextEditingController? - 控制器,用于读取/设置文字
decoration InputDecoration? - 外观装饰
keyboardType TextInputType? - 键盘类型
obscureText bool false 是否隐藏文字(密码输入)
maxLines int? 1 最大行数,为 null 则不限
minLines int? - 最小行数
maxLength int? - 最大字符数
onChanged Function(String)? - 文字变化回调
onSubmitted Function(String)? - 提交(回车)回调
onTap VoidCallback? - 点击回调
enabled bool? true 是否可用
autofocus bool false 是否自动聚焦
focusNode FocusNode? - 焦点控制
style TextStyle? - 文字样式
textAlign TextAlign start 对齐方式
readOnly bool false 只读模式

InputDecoration 参数表

参数 类型 说明
labelText String? 标签文字(聚焦时上移)
hintText String? 占位提示文字
helperText String? 下方帮助文字
errorText String? 错误提示文字(红色)
prefixIcon Widget? 左侧图标
suffixIcon Widget? 右侧图标
prefixText String? 左侧文字
suffixText String? 右侧文字
border InputBorder? 边框
filled bool? 是否填充背景色
fillColor Color? 填充背景色
contentPadding EdgeInsetsGeometry? 内容内边距
// TextEditingController 用法
class LoginForm extends StatefulWidget {
  const LoginForm({super.key});

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  // 1. 创建控制器
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  void dispose() {
    // 2. 销毁时释放资源(重要!防止内存泄漏)
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _login() {
    // 3. 读取文字
    final email = _emailController.text.trim();
    final password = _passwordController.text;
    print('邮箱:$email,密码:$password');
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _emailController,
          keyboardType: TextInputType.emailAddress,
          decoration: const InputDecoration(
            labelText: '邮箱',
            hintText: '请输入邮箱地址',
            prefixIcon: Icon(Icons.email),
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 16),
        TextField(
          controller: _passwordController,
          obscureText: true,              // 密码隐藏
          decoration: InputDecoration(
            labelText: '密码',
            hintText: '请输入密码',
            prefixIcon: const Icon(Icons.lock),
            border: const OutlineInputBorder(),
            suffixIcon: IconButton(       // 显示/隐藏密码按钮
              onPressed: () {},
              icon: const Icon(Icons.visibility),
            ),
          ),
          onSubmitted: (_) => _login(),   // 回车触发登录
        ),
        const SizedBox(height: 24),
        ElevatedButton(
          onPressed: _login,
          child: const Text('登录'),
        ),
      ],
    );
  }
}

ListView

参数表

参数 类型 默认值 说明
children List<Widget>? - 静态子列表(少量数据用)
scrollDirection Axis vertical 滚动方向
reverse bool false 是否反向排列
physics ScrollPhysics? - 滚动物理效果
shrinkWrap bool false 是否根据内容计算高度
padding EdgeInsetsGeometry? - 列表内边距
itemExtent double? - 固定子项高度(优化性能)
primary bool? - 是否是主滚动视图

ListView.builder 参数表

参数 类型 默认值 说明
itemCount int? - 总项数,不填则无限
itemBuilder IndexedWidgetBuilder 必填 构建每项的函数
scrollDirection Axis vertical 滚动方向
physics ScrollPhysics? - 滚动物理效果
shrinkWrap bool false 是否根据内容计算高度
// 静态列表(数据量少时使用)
ListView(
  padding: const EdgeInsets.all(16),
  children: const [
    ListTile(
      leading: Icon(Icons.home),
      title: Text('首页'),
      subtitle: Text('返回主页'),
      trailing: Icon(Icons.arrow_forward_ios),
    ),
    Divider(),                           // 分割线
    ListTile(
      leading: Icon(Icons.settings),
      title: Text('设置'),
    ),
  ],
)

// 动态列表(数据量大时使用,按需创建 Widget)
final List<String> items = List.generate(100, (i) => '列表项 ${i + 1}');

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      leading: CircleAvatar(child: Text('${index + 1}')),
      title: Text(items[index]),
      onTap: () => print('点击了 ${items[index]}'),
    );
  },
)

// 水平滚动列表
SizedBox(
  height: 120,
  child: ListView.builder(
    scrollDirection: Axis.horizontal,    // 水平滚动
    itemCount: 10,
    itemBuilder: (context, index) {
      return Container(
        width: 100,
        margin: const EdgeInsets.only(right: 8),
        decoration: BoxDecoration(
          color: Colors.blue.shade100,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Center(child: Text('卡片 $index')),
      );
    },
  ),
)

GridView.builder

gridDelegate 说明

类型 说明
SliverGridDelegateWithFixedCrossAxisCount 固定列数
SliverGridDelegateWithMaxCrossAxisExtent 固定每项最大宽度

SliverGridDelegateWithFixedCrossAxisCount 参数

参数 类型 说明
crossAxisCount int 列数(必填)
mainAxisSpacing double 主轴方向间距
crossAxisSpacing double 交叉轴方向间距
childAspectRatio double 子项宽高比(默认 1.0)
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,          // 3 列
    crossAxisSpacing: 8,        // 列间距
    mainAxisSpacing: 8,         // 行间距
    childAspectRatio: 1.0,      // 宽高比 1:1(正方形)
  ),
  itemCount: 30,
  itemBuilder: (context, index) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.primaries[index % Colors.primaries.length].shade200,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Center(child: Text('$index')),
    );
  },
)

SingleChildScrollView

当内容可能超出屏幕时,包裹在 SingleChildScrollView 中使其可滚动。

SingleChildScrollView(
  padding: const EdgeInsets.all(16),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      const Text('表单标题', style: TextStyle(fontSize: 24)),
      const SizedBox(height: 16),
      const TextField(decoration: InputDecoration(labelText: '姓名')),
      const SizedBox(height: 16),
      const TextField(decoration: InputDecoration(labelText: '邮箱')),
      const SizedBox(height: 16),
      const TextField(
        maxLines: 5,
        decoration: InputDecoration(
          labelText: '备注',
          border: OutlineInputBorder(),
        ),
      ),
      const SizedBox(height: 24),
      ElevatedButton(onPressed: () {}, child: const Text('提交')),
    ],
  ),
)

AppBar

参数表

参数 类型 说明
title Widget? 标题 Widget
leading Widget? 左侧 Widget(默认返回按钮或菜单图标)
actions List<Widget>? 右侧操作按钮列表
bottom PreferredSizeWidget? 底部 Widget(常用 TabBar)
backgroundColor Color? 背景色
foregroundColor Color? 前景色(标题、图标)
elevation double? 阴影高度
centerTitle bool? 标题是否居中
automaticallyImplyLeading bool 是否自动添加返回按钮
AppBar(
  title: const Text('文章详情'),
  leading: IconButton(
    icon: const Icon(Icons.arrow_back),
    onPressed: () => Navigator.pop(context),
  ),
  actions: [
    IconButton(
      icon: const Icon(Icons.share),
      onPressed: () {},
    ),
    IconButton(
      icon: const Icon(Icons.more_vert),
      onPressed: () {},
    ),
  ],
  bottom: const TabBar(
    tabs: [
      Tab(text: '推荐'),
      Tab(text: '最新'),
      Tab(text: '热门'),
    ],
  ),
)

BottomNavigationBar / NavigationBar

// BottomNavigationBar(经典样式)
class MainPage extends StatefulWidget {
  const MainPage({super.key});

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;

  final List<Widget> _pages = [
    const HomePage(),
    const FavoritePage(),
    const ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) => setState(() => _currentIndex = index),
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home_outlined),
            activeIcon: Icon(Icons.home),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.favorite_outline),
            activeIcon: Icon(Icons.favorite),
            label: '收藏',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person_outline),
            activeIcon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

// NavigationBar(Flutter 3+ Material You 风格,推荐)
NavigationBar(
  selectedIndex: _currentIndex,
  onDestinationSelected: (index) => setState(() => _currentIndex = index),
  destinations: const [
    NavigationDestination(
      icon: Icon(Icons.home_outlined),
      selectedIcon: Icon(Icons.home),
      label: '首页',
    ),
    NavigationDestination(
      icon: Icon(Icons.favorite_outline),
      selectedIcon: Icon(Icons.favorite),
      label: '收藏',
    ),
    NavigationDestination(
      icon: Icon(Icons.person_outline),
      selectedIcon: Icon(Icons.person),
      label: '我的',
    ),
  ],
)

Drawer

Scaffold(
  appBar: AppBar(title: const Text('首页')),
  drawer: Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: [
        // 抽屉头部
        UserAccountsDrawerHeader(
          accountName: const Text('张三'),
          accountEmail: const Text('[email protected]'),
          currentAccountPicture: const CircleAvatar(
            backgroundColor: Colors.white,
            child: Icon(Icons.person, size: 40),
          ),
          decoration: const BoxDecoration(color: Colors.blue),
        ),
        // 菜单项
        ListTile(
          leading: const Icon(Icons.home),
          title: const Text('首页'),
          onTap: () {
            Navigator.pop(context); // 关闭抽屉
          },
        ),
        ListTile(
          leading: const Icon(Icons.settings),
          title: const Text('设置'),
          onTap: () {
            Navigator.pop(context);
            Navigator.pushNamed(context, '/settings');
          },
        ),
        const Divider(),
        ListTile(
          leading: const Icon(Icons.logout),
          title: const Text('退出登录'),
          onTap: () {},
        ),
      ],
    ),
  ),
  body: const Center(child: Text('内容区域')),
)

状态管理

setState 基础用法

class ShoppingCart extends StatefulWidget {
  const ShoppingCart({super.key});

  @override
  State<ShoppingCart> createState() => _ShoppingCartState();
}

class _ShoppingCartState extends State<ShoppingCart> {
  final List<String> _items = [];

  void _addItem(String item) {
    // setState 内部同步修改状态,Flutter 会在下一帧重建 Widget
    setState(() {
      _items.add(item);
    });
  }

  void _removeItem(int index) {
    setState(() {
      _items.removeAt(index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('购物车(${_items.length})')),
      body: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(_items[index]),
          trailing: IconButton(
            icon: const Icon(Icons.delete),
            onPressed: () => _removeItem(index),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _addItem('商品 ${_items.length + 1}'),
        child: const Icon(Icons.add),
      ),
    );
  }
}

setState 注意事项

  • 不要在 build 方法中调用 setState
  • setState 内部应只做状态修改,不做耗时操作
  • 异步操作完成后调用 setState 前,先检查 mounted(防止组件已销毁)
Future<void> _loadData() async {
  final data = await fetchDataFromNetwork();
  // 异步完成后检查组件是否还在树中
  if (!mounted) return;
  setState(() {
    _data = data;
  });
}

InheritedWidget 原理

InheritedWidget 是 Flutter 内置的状态向下传递机制,Provider 等状态管理库都基于它实现。它允许子 Widget 在 Widget 树中向上查找祖先数据,而无需逐层传递参数。

// 简单示例(理解原理,实际使用 Provider)
class AppState extends InheritedWidget {
  final int counter;

  const AppState({
    super.key,
    required this.counter,
    required super.child,
  });

  // 子 Widget 通过 of 方法获取数据
  static AppState of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AppState>()!;
  }

  // 返回 true 时,依赖此 Widget 的子 Widget 会重建
  @override
  bool updateShouldNotify(AppState oldWidget) {
    return counter != oldWidget.counter;
  }
}

Provider

Provider 是官方推荐的轻量级状态管理方案。

# 添加依赖
flutter pub add provider

核心 API 参数表

类/方法 参数 说明
ChangeNotifierProvider create(context) => T 创建 ChangeNotifier 实例
ChangeNotifierProvider childWidget 子 Widget 树
Consumer<T> builder(context, T value, child) 监听状态变化并重建
context.watch<T>() 无参数 监听 T 的变化,变化时重建当前 Widget
context.read<T>() 无参数 读取 T,不监听变化(用于事件处理)
context.select<T, R>() selector(T) => R 只监听 T 的某个字段变化
// 1. 定义状态类
class FavoriteModel extends ChangeNotifier {
  final List<String> _favorites = [];

  List<String> get favorites => List.unmodifiable(_favorites);

  bool isFavorite(String id) => _favorites.contains(id);

  void toggle(String id) {
    if (_favorites.contains(id)) {
      _favorites.remove(id);
    } else {
      _favorites.add(id);
    }
    notifyListeners(); // 通知所有监听者重建
  }
}

// 2. 在根 Widget 提供状态
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => FavoriteModel(),
      child: const MyApp(),
    ),
  );
}

// 3. 在子 Widget 中使用
class ArticleCard extends StatelessWidget {
  final String articleId;
  final String title;

  const ArticleCard({super.key, required this.articleId, required this.title});

  @override
  Widget build(BuildContext context) {
    // context.watch 监听变化,isFavorite 改变时重建
    final isFavorite = context.watch<FavoriteModel>().isFavorite(articleId);

    return ListTile(
      title: Text(title),
      trailing: IconButton(
        icon: Icon(
          isFavorite ? Icons.favorite : Icons.favorite_border,
          color: isFavorite ? Colors.red : null,
        ),
        onPressed: () {
          // context.read 不监听,只执行操作
          context.read<FavoriteModel>().toggle(articleId);
        },
      ),
    );
  }
}

// 提供多个状态(MultiProvider)
MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => FavoriteModel()),
    ChangeNotifierProvider(create: (_) => UserModel()),
  ],
  child: const MyApp(),
)

Riverpod 简介

Riverpod 是 Provider 的升级版,解决了 Provider 的一些限制,是目前最受推荐的状态管理方案之一。

flutter pub add flutter_riverpod
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 定义 Provider
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);

  void increment() => state++;
  void decrement() => state--;
}

// 根 Widget 包裹 ProviderScope
void main() {
  runApp(const ProviderScope(child: MyApp()));
}

// 使用状态:ConsumerWidget 替代 StatelessWidget
class CounterPage extends ConsumerWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider); // 监听

    return Scaffold(
      body: Center(child: Text('$count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).increment(),
        child: const Icon(Icons.add),
      ),
    );
  }
}

导航与路由

参数表

方法 参数 说明
Navigator.push context, route 进入新页面
Navigator.pop context, [result] 返回上一页,可传返回值
Navigator.pushReplacement context, route 替换当前页面
Navigator.pushAndRemoveUntil context, route, predicate 清空栈并跳转
Navigator.canPop context 是否可以返回

MaterialPageRoute 参数表

参数 类型 说明
builder (context) => Widget 目标页面构建函数(必填)
settings RouteSettings? 路由设置(名称、参数)
fullscreenDialog bool 是否全屏弹出(iOS 样式)
maintainState bool 是否保持状态(默认 true)
// 基础跳转
ElevatedButton(
  onPressed: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => const DetailPage(id: '123'),
      ),
    );
  },
  child: const Text('查看详情'),
)

// 接收返回值
ElevatedButton(
  onPressed: () async {
    final result = await Navigator.push<String>(
      context,
      MaterialPageRoute(builder: (context) => const SelectPage()),
    );
    if (result != null) {
      print('用户选择了:$result');
    }
  },
  child: const Text('选择'),
)

// 返回时传值
Navigator.pop(context, '用户的选择');

// 替换当前页(登录后跳转首页,不允许返回登录页)
Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => const HomePage()),
);

// 清空栈并跳转(退出登录)
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => const LoginPage()),
  (route) => false, // false 表示清空所有
);

命名路由

// 在 MaterialApp 配置路由表
MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => const HomePage(),
    '/detail': (context) => const DetailPage(),
    '/settings': (context) => const SettingsPage(),
  },
)

// 跳转
Navigator.pushNamed(context, '/detail');

// 传参(通过 arguments)
Navigator.pushNamed(
  context,
  '/detail',
  arguments: {'id': '123', 'title': '文章标题'},
);

// 接收参数
class DetailPage extends StatelessWidget {
  const DetailPage({super.key});

  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments
        as Map<String, String>;
    final id = args['id'];
    final title = args['title'];

    return Scaffold(
      appBar: AppBar(title: Text(title ?? '')),
      body: Text('文章 ID:$id'),
    );
  }
}

go_router(推荐)

go_router 是 Flutter 官方维护的声明式路由库,支持 URL 路由、嵌套路由、重定向等。

flutter pub add go_router
import 'package:go_router/go_router.dart';

// 配置路由
final GoRouter router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
    ),
    GoRoute(
      path: '/detail/:id',               // 路径参数
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return DetailPage(id: id);
      },
    ),
    GoRoute(
      path: '/search',
      builder: (context, state) {
        final query = state.uri.queryParameters['q'] ?? '';
        return SearchPage(query: query);
      },
    ),
  ],
  // 重定向(例如未登录跳转登录页)
  redirect: (context, state) {
    final isLoggedIn = UserService.isLoggedIn;
    if (!isLoggedIn && state.matchedLocation != '/login') {
      return '/login';
    }
    return null; // 不重定向
  },
);

// 在 MaterialApp.router 使用
MaterialApp.router(routerConfig: router)

// 跳转
context.go('/');                          // 替换当前路由
context.push('/detail/123');             // 叠加路由(可返回)
context.pop();                            // 返回
context.go('/search?q=flutter');         // 带查询参数

网络请求

http 包

flutter pub add http

参数表

方法 必填参数 可选参数 说明
http.get Uri url Map<String, String>? headers GET 请求
http.post Uri url headers, body, encoding POST 请求
http.put Uri url headers, body, encoding PUT 请求
http.delete Uri url headers, body DELETE 请求
import 'dart:convert';
import 'package:http/http.dart' as http;

// 模型类
class Article {
  final int id;
  final String title;
  final String body;

  Article({required this.id, required this.title, required this.body});

  // 从 JSON 创建对象
  factory Article.fromJson(Map<String, dynamic> json) {
    return Article(
      id: json['id'] as int,
      title: json['title'] as String,
      body: json['body'] as String,
    );
  }

  // 转为 JSON
  Map<String, dynamic> toJson() => {
    'id': id,
    'title': title,
    'body': body,
  };
}

// GET 请求
Future<List<Article>> fetchArticles() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer your_token',
    },
  );

  if (response.statusCode == 200) {
    final List<dynamic> jsonList = jsonDecode(response.body);
    return jsonList.map((json) => Article.fromJson(json)).toList();
  } else {
    throw Exception('请求失败:${response.statusCode}');
  }
}

// POST 请求
Future<Article> createArticle(String title, String body) async {
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({'title': title, 'body': body, 'userId': 1}),
  );

  if (response.statusCode == 201) {
    return Article.fromJson(jsonDecode(response.body));
  } else {
    throw Exception('创建失败:${response.statusCode}');
  }
}

dio 包

dio 功能更强大,支持拦截器、文件上传下载、FormData 等。

flutter pub add dio

BaseOptions 参数表

参数 类型 说明
baseUrl String? 基础 URL
connectTimeout Duration? 连接超时
receiveTimeout Duration? 接收超时
sendTimeout Duration? 发送超时
headers Map<String, dynamic>? 默认请求头
contentType String? 默认 Content-Type
responseType ResponseType 响应类型(json/stream/plain/bytes)
import 'package:dio/dio.dart';

// 封装 API 客户端
class ApiClient {
  static final Dio _dio = Dio(
    BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 15),
      headers: {'Content-Type': 'application/json'},
    ),
  )
    // 请求/响应拦截器
    ..interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          // 在请求头中自动加入 Token
          options.headers['Authorization'] = 'Bearer ${UserService.token}';
          handler.next(options); // 继续请求
        },
        onResponse: (response, handler) {
          // 统一处理响应
          handler.next(response);
        },
        onError: (error, handler) {
          // 统一处理错误(如 401 跳转登录)
          if (error.response?.statusCode == 401) {
            UserService.logout();
          }
          handler.next(error);
        },
      ),
    )
    // 日志拦截器
    ..interceptors.add(LogInterceptor(responseBody: true));

  static Future<List<Article>> getArticles() async {
    try {
      final response = await _dio.get('/articles');
      return (response.data as List)
          .map((json) => Article.fromJson(json))
          .toList();
    } on DioException catch (e) {
      throw Exception('请求失败:${e.message}');
    }
  }

  static Future<void> uploadFile(String filePath) async {
    final formData = FormData.fromMap({
      'file': await MultipartFile.fromFile(filePath, filename: 'upload.jpg'),
    });
    await _dio.post('/upload', data: formData);
  }
}

本地存储

shared_preferences

适合存储简单的键值对(用户偏好设置、Token 等)。

flutter pub add shared_preferences

参数表

方法 参数 说明
getString(key) String key 读取字符串
setString(key, value) String key, String value 写入字符串
getInt(key) String key 读取整数
setInt(key, value) String key, int value 写入整数
getBool(key) String key 读取布尔值
setBool(key, value) String key, bool value 写入布尔值
getDouble(key) String key 读取浮点数
getStringList(key) String key 读取字符串列表
remove(key) String key 删除指定键
clear() 清除所有数据
containsKey(key) String key 是否包含指定键
import 'package:shared_preferences/shared_preferences.dart';

class PrefsService {
  static const _keyToken = 'auth_token';
  static const _keyDarkMode = 'dark_mode';

  // 保存 Token
  static Future<void> saveToken(String token) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_keyToken, token);
  }

  // 读取 Token
  static Future<String?> getToken() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(_keyToken);
  }

  // 保存深色模式偏好
  static Future<void> setDarkMode(bool enabled) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_keyDarkMode, enabled);
  }

  static Future<bool> getDarkMode() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_keyDarkMode) ?? false; // 默认 false
  }

  // 清除登录信息
  static Future<void> clearAuth() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_keyToken);
  }
}

sqflite 基础

适合存储结构化数据。

flutter pub add sqflite path
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseService {
  static Database? _db;

  static Future<Database> get database async {
    _db ??= await _initDatabase();
    return _db!;
  }

  static Future<Database> _initDatabase() async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, 'app.db');

    return openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        await db.execute('''
          CREATE TABLE articles (
            id INTEGER PRIMARY KEY,
            title TEXT NOT NULL,
            body TEXT,
            is_favorite INTEGER DEFAULT 0
          )
        ''');
      },
    );
  }

  // 插入
  static Future<int> insertArticle(Map<String, dynamic> article) async {
    final db = await database;
    return db.insert('articles', article,
        conflictAlgorithm: ConflictAlgorithm.replace);
  }

  // 查询全部
  static Future<List<Map<String, dynamic>>> getArticles() async {
    final db = await database;
    return db.query('articles', orderBy: 'id DESC');
  }

  // 查询收藏
  static Future<List<Map<String, dynamic>>> getFavorites() async {
    final db = await database;
    return db.query('articles', where: 'is_favorite = ?', whereArgs: [1]);
  }

  // 更新
  static Future<int> updateFavorite(int id, bool isFavorite) async {
    final db = await database;
    return db.update(
      'articles',
      {'is_favorite': isFavorite ? 1 : 0},
      where: 'id = ?',
      whereArgs: [id],
    );
  }

  // 删除
  static Future<int> deleteArticle(int id) async {
    final db = await database;
    return db.delete('articles', where: 'id = ?', whereArgs: [id]);
  }
}

常用 Widget 进阶

Form 表单验证

class RegisterForm extends StatefulWidget {
  const RegisterForm({super.key});

  @override
  State<RegisterForm> createState() => _RegisterFormState();
}

class _RegisterFormState extends State<RegisterForm> {
  // GlobalKey 用于访问 FormState
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _submit() {
    // 触发所有 TextFormField 的 validator
    if (_formKey.currentState!.validate()) {
      print('表单验证通过');
      print('邮箱:${_emailController.text}');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _emailController,
            keyboardType: TextInputType.emailAddress,
            decoration: const InputDecoration(
              labelText: '邮箱',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '请输入邮箱';
              }
              if (!value.contains('@')) {
                return '邮箱格式不正确';
              }
              return null; // 返回 null 表示验证通过
            },
          ),
          const SizedBox(height: 16),
          TextFormField(
            controller: _passwordController,
            obscureText: true,
            decoration: const InputDecoration(
              labelText: '密码',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.length < 6) {
                return '密码不能少于 6 位';
              }
              return null;
            },
          ),
          const SizedBox(height: 24),
          ElevatedButton(onPressed: _submit, child: const Text('注册')),
        ],
      ),
    );
  }
}

FutureBuilder

用于处理异步数据加载,根据 Future 的状态显示不同 UI。

参数表

参数 类型 说明
future Future<T>? 要监听的 Future
builder (context, AsyncSnapshot<T>) => Widget 构建函数(必填)
initialData T? 初始数据(Future 完成前显示)

AsyncSnapshot 状态

snapshot.connectionState 说明
ConnectionState.none 无 Future
ConnectionState.waiting 等待中
ConnectionState.active 进行中(Stream 用)
ConnectionState.done 完成
FutureBuilder<List<Article>>(
  future: ApiClient.getArticles(),
  builder: (context, snapshot) {
    // 加载中
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    }

    // 发生错误
    if (snapshot.hasError) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error, size: 48, color: Colors.red),
            const SizedBox(height: 16),
            Text('加载失败:${snapshot.error}'),
            ElevatedButton(
              onPressed: () => setState(() {}), // 刷新
              child: const Text('重试'),
            ),
          ],
        ),
      );
    }

    // 数据为空
    if (!snapshot.hasData || snapshot.data!.isEmpty) {
      return const Center(child: Text('暂无数据'));
    }

    // 显示数据
    final articles = snapshot.data!;
    return ListView.builder(
      itemCount: articles.length,
      itemBuilder: (context, index) => ListTile(
        title: Text(articles[index].title),
      ),
    );
  },
)

StreamBuilder

与 FutureBuilder 类似,但用于持续更新的数据流(WebSocket、实时数据库等)。

StreamBuilder<int>(
  stream: Stream.periodic(
    const Duration(seconds: 1),
    (count) => count,
  ),
  builder: (context, snapshot) {
    if (!snapshot.hasData) return const Text('等待数据...');
    return Text('已过 ${snapshot.data} 秒');
  },
)

GestureDetector

参数表

参数 类型 说明
onTap VoidCallback? 单击
onDoubleTap VoidCallback? 双击
onLongPress VoidCallback? 长按
onPanUpdate Function(DragUpdateDetails)? 拖动
onScaleUpdate Function(ScaleUpdateDetails)? 缩放
behavior HitTestBehavior? 命中测试行为
child Widget? 子 Widget
GestureDetector(
  onTap: () => print('单击'),
  onDoubleTap: () => print('双击'),
  onLongPress: () => print('长按'),
  // 拖动
  onPanUpdate: (details) {
    print('拖动:dx=${details.delta.dx}, dy=${details.delta.dy}');
  },
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
    child: const Center(child: Text('点我')),
  ),
)

AnimatedContainer

AnimatedContainer 会自动对属性变化进行动画过渡。

class AnimatedBox extends StatefulWidget {
  const AnimatedBox({super.key});

  @override
  State<AnimatedBox> createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300), // 动画时长
        curve: Curves.easeInOut,                      // 缓动曲线
        width: _expanded ? 200 : 100,
        height: _expanded ? 200 : 100,
        color: _expanded ? Colors.blue : Colors.red,
        child: const Center(child: Text('点击')),
      ),
    );
  }
}

Hero 动画

Hero 实现两个页面之间的共享元素过渡动画。

// 第一个页面
Hero(
  tag: 'article-image-${article.id}', // tag 必须唯一
  child: Image.network(article.imageUrl, fit: BoxFit.cover),
)

// 第二个页面(使用相同 tag)
Hero(
  tag: 'article-image-${article.id}',
  child: Image.network(article.imageUrl, fit: BoxFit.contain),
)

AnimatedOpacity

class FadeInWidget extends StatefulWidget {
  const FadeInWidget({super.key});

  @override
  State<FadeInWidget> createState() => _FadeInWidgetState();
}

class _FadeInWidgetState extends State<FadeInWidget> {
  double _opacity = 0;

  @override
  void initState() {
    super.initState();
    // 显示后淡入
    Future.delayed(
      const Duration(milliseconds: 100),
      () => setState(() => _opacity = 1.0),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      duration: const Duration(milliseconds: 500),
      opacity: _opacity,
      child: const Text('淡入显示的内容'),
    );
  }
}

主题与样式

ThemeData 参数表

参数 类型 说明
colorScheme ColorScheme? Material 3 颜色方案
useMaterial3 bool 是否使用 Material 3
textTheme TextTheme? 文字主题
appBarTheme AppBarTheme? AppBar 全局样式
elevatedButtonTheme ElevatedButtonThemeData? ElevatedButton 全局样式
inputDecorationTheme InputDecorationTheme? TextField 全局样式
cardTheme CardTheme? Card 全局样式
scaffoldBackgroundColor Color? Scaffold 背景色
MaterialApp(
  theme: ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.blue,
      brightness: Brightness.light,
    ),
    textTheme: const TextTheme(
      headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
      bodyLarge: TextStyle(fontSize: 16, height: 1.6),
    ),
    appBarTheme: const AppBarTheme(
      centerTitle: true,
      elevation: 0,
    ),
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        minimumSize: const Size(double.infinity, 48), // 按钮默认全宽
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
    ),
    inputDecorationTheme: const InputDecorationTheme(
      border: OutlineInputBorder(),
      contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    ),
  ),
  darkTheme: ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.blue,
      brightness: Brightness.dark,
    ),
  ),
  themeMode: ThemeMode.system, // 跟随系统深色模式
  home: const HomePage(),
)

在 Widget 中读取主题:

// 读取当前主题
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;

Text(
  '标题',
  style: theme.textTheme.headlineMedium?.copyWith(
    color: colorScheme.primary,
  ),
)

响应式布局

MediaQuery

Widget build(BuildContext context) {
  final size = MediaQuery.sizeOf(context);      // 屏幕尺寸
  final padding = MediaQuery.paddingOf(context); // 安全区域(刘海、导航栏)

  // 根据屏幕宽度决定布局
  final isTablet = size.width > 600;

  return isTablet
      ? Row(children: [sidebar, content])       // 平板:左右布局
      : Column(children: [content]);             // 手机:上下布局
}

LayoutBuilder

LayoutBuilder 根据父容器约束来布局,比 MediaQuery 更准确。

LayoutBuilder(
  builder: (context, constraints) {
    if (constraints.maxWidth > 600) {
      // 宽屏:两列网格
      return GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
        ),
        itemCount: 10,
        itemBuilder: (context, index) => Card(child: Text('$index')),
      );
    } else {
      // 窄屏:单列列表
      return ListView.builder(
        itemCount: 10,
        itemBuilder: (context, index) => ListTile(title: Text('$index')),
      );
    }
  },
)

综合实战:新闻阅读 App

本节实现一个包含底部导航、文章列表、详情页、收藏功能的完整 App。

项目结构

lib/
├── main.dart
├── models/
│   └── article.dart          # 文章数据模型
├── providers/
│   └── favorite_provider.dart # 收藏状态管理
├── services/
│   └── news_service.dart      # 网络请求
└── pages/
    ├── main_page.dart         # 底部导航容器
    ├── home_page.dart         # 首页(文章列表)
    ├── detail_page.dart       # 文章详情
    ├── favorite_page.dart     # 收藏列表
    └── profile_page.dart      # 我的

models/article.dart

class Article {
  final int id;
  final String title;
  final String body;
  final String author;
  final String imageUrl;

  const Article({
    required this.id,
    required this.title,
    required this.body,
    required this.author,
    required this.imageUrl,
  });

  factory Article.fromJson(Map<String, dynamic> json) {
    return Article(
      id: json['id'] as int,
      title: json['title'] as String,
      body: json['body'] as String,
      author: 'User ${json['userId']}',
      imageUrl: 'https://picsum.photos/seed/${json['id']}/400/200',
    );
  }
}

providers/favorite_provider.dart

import 'package:flutter/foundation.dart';
import '../models/article.dart';

class FavoriteProvider extends ChangeNotifier {
  final List<Article> _favorites = [];

  List<Article> get favorites => List.unmodifiable(_favorites);

  bool isFavorite(int id) => _favorites.any((a) => a.id == id);

  void toggle(Article article) {
    if (isFavorite(article.id)) {
      _favorites.removeWhere((a) => a.id == article.id);
    } else {
      _favorites.add(article);
    }
    notifyListeners();
  }
}

services/news_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/article.dart';

class NewsService {
  static const _baseUrl = 'https://jsonplaceholder.typicode.com';

  static Future<List<Article>> getArticles({int page = 1}) async {
    final response = await http.get(
      Uri.parse('$_baseUrl/posts?_page=$page&_limit=20'),
    );

    if (response.statusCode == 200) {
      final List<dynamic> jsonList = jsonDecode(response.body);
      return jsonList.map((json) => Article.fromJson(json)).toList();
    } else {
      throw Exception('加载文章失败:${response.statusCode}');
    }
  }
}

main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'pages/main_page.dart';
import 'providers/favorite_provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => FavoriteProvider(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '新闻阅读',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
      ),
      home: const MainPage(),
    );
  }
}

pages/main_page.dart

import 'package:flutter/material.dart';
import 'home_page.dart';
import 'favorite_page.dart';
import 'profile_page.dart';

class MainPage extends StatefulWidget {
  const MainPage({super.key});

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;

  final List<Widget> _pages = const [
    HomePage(),
    FavoritePage(),
    ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(         // 保持各页面状态(不用 _pages[_currentIndex])
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() => _currentIndex = index);
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: '首页',
          ),
          NavigationDestination(
            icon: Icon(Icons.favorite_outline),
            selectedIcon: Icon(Icons.favorite),
            label: '收藏',
          ),
          NavigationDestination(
            icon: Icon(Icons.person_outline),
            selectedIcon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

pages/home_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/article.dart';
import '../providers/favorite_provider.dart';
import '../services/news_service.dart';
import 'detail_page.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late Future<List<Article>> _articlesFuture;

  @override
  void initState() {
    super.initState();
    _articlesFuture = NewsService.getArticles();
  }

  void _refresh() {
    setState(() {
      _articlesFuture = NewsService.getArticles();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('今日新闻'),
        actions: [
          IconButton(icon: const Icon(Icons.refresh), onPressed: _refresh),
        ],
      ),
      body: FutureBuilder<List<Article>>(
        future: _articlesFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(Icons.cloud_off, size: 64, color: Colors.grey),
                  const SizedBox(height: 16),
                  Text('${snapshot.error}'),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: _refresh,
                    child: const Text('重试'),
                  ),
                ],
              ),
            );
          }

          final articles = snapshot.data ?? [];
          return RefreshIndicator(            // 下拉刷新
            onRefresh: () async => _refresh(),
            child: ListView.separated(
              itemCount: articles.length,
              separatorBuilder: (_, __) => const Divider(height: 1),
              itemBuilder: (context, index) {
                return _ArticleCard(article: articles[index]);
              },
            ),
          );
        },
      ),
    );
  }
}

class _ArticleCard extends StatelessWidget {
  final Article article;

  const _ArticleCard({required this.article});

  @override
  Widget build(BuildContext context) {
    // 使用 context.select 只监听该文章的收藏状态,避免不必要的重建
    final isFavorite = context.select<FavoriteProvider, bool>(
      (p) => p.isFavorite(article.id),
    );

    return InkWell(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => DetailPage(article: article),
          ),
        );
      },
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 文章图片
            Hero(
              tag: 'article-image-${article.id}',
              child: ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: Image.network(
                  article.imageUrl,
                  width: 80,
                  height: 80,
                  fit: BoxFit.cover,
                  errorBuilder: (_, __, ___) => Container(
                    width: 80,
                    height: 80,
                    color: Colors.grey.shade200,
                    child: const Icon(Icons.image),
                  ),
                ),
              ),
            ),
            const SizedBox(width: 12),
            // 文章信息
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    article.title,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.w600,
                    ),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    article.author,
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.grey.shade600,
                    ),
                  ),
                ],
              ),
            ),
            // 收藏按钮
            IconButton(
              icon: Icon(
                isFavorite ? Icons.favorite : Icons.favorite_border,
                color: isFavorite ? Colors.red : Colors.grey,
              ),
              onPressed: () {
                context.read<FavoriteProvider>().toggle(article);
              },
            ),
          ],
        ),
      ),
    );
  }
}

pages/detail_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/article.dart';
import '../providers/favorite_provider.dart';

class DetailPage extends StatelessWidget {
  final Article article;

  const DetailPage({super.key, required this.article});

  @override
  Widget build(BuildContext context) {
    final isFavorite = context.watch<FavoriteProvider>().isFavorite(article.id);

    return Scaffold(
      appBar: AppBar(
        title: const Text('文章详情'),
        actions: [
          IconButton(
            icon: Icon(
              isFavorite ? Icons.favorite : Icons.favorite_border,
              color: isFavorite ? Colors.red : null,
            ),
            onPressed: () {
              context.read<FavoriteProvider>().toggle(article);
            },
          ),
        ],
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 文章头图(Hero 动画)
            Hero(
              tag: 'article-image-${article.id}',
              child: Image.network(
                article.imageUrl,
                width: double.infinity,
                height: 220,
                fit: BoxFit.cover,
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    article.title,
                    style: const TextStyle(
                      fontSize: 22,
                      fontWeight: FontWeight.bold,
                      height: 1.4,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    article.author,
                    style: TextStyle(
                      color: Colors.grey.shade600,
                      fontSize: 14,
                    ),
                  ),
                  const Divider(height: 32),
                  Text(
                    article.body,
                    style: const TextStyle(fontSize: 16, height: 1.8),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

pages/favorite_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/favorite_provider.dart';
import 'detail_page.dart';

class FavoritePage extends StatelessWidget {
  const FavoritePage({super.key});

  @override
  Widget build(BuildContext context) {
    final favorites = context.watch<FavoriteProvider>().favorites;

    return Scaffold(
      appBar: AppBar(title: const Text('我的收藏')),
      body: favorites.isEmpty
          ? const Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.favorite_border, size: 64, color: Colors.grey),
                  SizedBox(height: 16),
                  Text('还没有收藏文章', style: TextStyle(color: Colors.grey)),
                ],
              ),
            )
          : ListView.builder(
              itemCount: favorites.length,
              itemBuilder: (context, index) {
                final article = favorites[index];
                return ListTile(
                  leading: ClipRRect(
                    borderRadius: BorderRadius.circular(4),
                    child: Image.network(
                      article.imageUrl,
                      width: 56,
                      height: 56,
                      fit: BoxFit.cover,
                    ),
                  ),
                  title: Text(
                    article.title,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  subtitle: Text(article.author),
                  trailing: IconButton(
                    icon: const Icon(Icons.favorite, color: Colors.red),
                    onPressed: () {
                      context.read<FavoriteProvider>().toggle(article);
                    },
                  ),
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => DetailPage(article: article),
                      ),
                    );
                  },
                );
              },
            ),
    );
  }
}

pages/profile_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/favorite_provider.dart';

class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    final favoriteCount = context.watch<FavoriteProvider>().favorites.length;

    return Scaffold(
      appBar: AppBar(title: const Text('我的')),
      body: Column(
        children: [
          // 用户信息卡片
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(24),
            color: Theme.of(context).colorScheme.primaryContainer,
            child: Column(
              children: [
                const CircleAvatar(
                  radius: 40,
                  child: Icon(Icons.person, size: 48),
                ),
                const SizedBox(height: 12),
                const Text(
                  '读者用户',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 4),
                Text('已收藏 $favoriteCount 篇文章'),
              ],
            ),
          ),
          const SizedBox(height: 16),
          // 设置列表
          ListTile(
            leading: const Icon(Icons.notifications),
            title: const Text('通知设置'),
            trailing: const Icon(Icons.arrow_forward_ios, size: 16),
            onTap: () {},
          ),
          ListTile(
            leading: const Icon(Icons.dark_mode),
            title: const Text('深色模式'),
            trailing: Switch(value: false, onChanged: (_) {}),
          ),
          ListTile(
            leading: const Icon(Icons.info),
            title: const Text('关于'),
            trailing: const Icon(Icons.arrow_forward_ios, size: 16),
            onTap: () {},
          ),
        ],
      ),
    );
  }
}

踩坑与注意事项

setState 在异步回调中的空安全问题

// 错误:组件已销毁后仍调用 setState
Future<void> _load() async {
  final data = await fetchData();
  setState(() => _data = data); // 组件可能已销毁,报错
}

// 正确:检查 mounted
Future<void> _load() async {
  final data = await fetchData();
  if (!mounted) return;
  setState(() => _data = data);
}

避免在 build 中创建对象

// 错误:每次 build 都创建新的 Future,导致 FutureBuilder 死循环
Widget build(BuildContext context) {
  return FutureBuilder(
    future: fetchData(), // 每次 build 都是新的 Future
    builder: (_, __) => ...,
  );
}

// 正确:在 initState 中创建
late Future<Data> _future;

@override
void initState() {
  super.initState();
  _future = fetchData();
}

Widget build(BuildContext context) {
  return FutureBuilder(future: _future, builder: (_, __) => ...);
}

TextEditingController 必须 dispose

@override
void dispose() {
  _controller.dispose(); // 忘记 dispose 会内存泄漏
  super.dispose();
}

ListView 在 Column 中的无界高度问题

// 错误:Column 中嵌套 ListView,高度无界报错
Column(
  children: [
    ListView(...), // 报错:unbounded height
  ],
)

// 正确方法 1:给 ListView 固定高度
Column(
  children: [
    SizedBox(
      height: 300,
      child: ListView(...),
    ),
  ],
)

// 正确方法 2:使用 shrinkWrap(性能较差,少量数据可用)
Column(
  children: [
    ListView(shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), ...),
  ],
)

// 正确方法 3:改用 CustomScrollView
CustomScrollView(
  slivers: [
    SliverToBoxAdapter(child: header),
    SliverList(...),
  ],
)

图片缓存

Image.network 不会自动缓存,推荐使用 cached_network_image 包:

flutter pub add cached_network_image
import 'package:cached_network_image/cached_network_image.dart';

CachedNetworkImage(
  imageUrl: 'https://example.com/image.jpg',
  placeholder: (context, url) => const CircularProgressIndicator(),
  errorWidget: (context, url, error) => const Icon(Icons.error),
)

最佳实践

优先用 const 构造 Widget:能用 const 的地方一律加 const,Flutter 会跳过这些节点的重建,是最简单的性能优化。

// 好:const 标记,不会因父 Widget 重建而重建
const Text('Hello');

// 差:每次父 rebuild 都新建实例
Text('Hello');

StatelessWidget 优先,setState 范围最小化:将有状态逻辑提取到独立的小 Widget,避免整棵树因局部状态变化重绘。复杂应用使用 Riverpod 或 Bloc 管理状态。

图片使用 cached_network_image:网络图片不走缓存会反复下载,CachedNetworkImage 自动处理内存缓存和磁盘缓存,生产环境必备。

Navigator 2.0 场景用 go_router:直接操作 Navigator.push 在深链接和 Web URL 同步场景下难以维护,go_router(官方推荐)提供声明式路由和统一的深链接处理。详见 GoRouter完全指南

异步操作用 FutureBuilder / StreamBuilder 显式处理加载态:不要直接在 initState 里赋值触发 setState,而是用 Builder 组件绑定 Future/Stream,让 UI 与数据状态同步。

FutureBuilder<User>(
  future: fetchUser(id),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }
    if (snapshot.hasError) return Text('Error: ${snapshot.error}');
    return UserCard(user: snapshot.data!);
  },
);

常见陷阱

陷阱:在 initState 中调用 context 相关方法

现象: 调用 Theme.of(context)Navigator.of(context) 时报错 dependOnInheritedWidgetOfExactType called during initState
原因: initState 执行时 Widget 尚未挂载到树上,context 没有可访问的上层 InheritedWidget。
解决: 将需要 context 的初始化代码移到 didChangeDependencies 或用 WidgetsBinding.instance.addPostFrameCallback

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  final theme = Theme.of(context); // 正确:此时 context 已可用
}

陷阱:setStatedispose 后调用

现象: 异步操作完成后调用 setState,控制台报 setState() called after dispose()
原因: Widget 已销毁但回调仍持有对它的引用并尝试更新状态。
解决: 在回调前检查 mounted

Future<void> loadData() async {
  final result = await fetchData();
  if (!mounted) return; // Widget 已销毁则跳过
  setState(() => data = result);
}

陷阱:ListView 子项高度不固定导致滚动性能差

现象: 长列表滚动时掉帧,或报 RenderFlex children have non-zero flex but incoming height constraints are unbounded
原因: 未指定 itemExtent 时 Flutter 需要布局每个 item 才能计算位置,大量 item 时 CPU 开销显著。
解决: 若 item 高度固定,设置 itemExtent;高度不一致时改用 ListView.builder 并配合 AutomaticKeepAliveClientMixin


参见

Dart入门
Flutter状态管理
GoRouter完全指南

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog