> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Flutter 入门
- URL: https://blog.vercanti.com/flutter-ru-men/
- Published: 2026-08-28T14:35:43.000Z
- Updated: 2026-08-28T14:59:27.000Z
- Description: 最后更新：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
- Author: yellowdog
- Tags: 移动开发

最后更新：2026-03-05

> 官方文档：<https://docs.flutter.dev/>  
> 适用版本：Flutter 3.x（2026-05-07 核实）

前置知识：[Dart入门](https://blog.vercanti.com/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%87%8D%E5%90%AF)
5. [Widget 核心概念](#widget-%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5)
6. [常用基础 Widget](#%E5%B8%B8%E7%94%A8%E5%9F%BA%E7%A1%80-widget)
7. [状态管理](#%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86)
8. [导航与路由](#%E5%AF%BC%E8%88%AA%E4%B8%8E%E8%B7%AF%E7%94%B1)
9. [网络请求](#%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82)
10. [本地存储](#%E6%9C%AC%E5%9C%B0%E5%AD%98%E5%82%A8)
11. [常用 Widget 进阶](#%E5%B8%B8%E7%94%A8-widget-%E8%BF%9B%E9%98%B6)
12. [主题与样式](#%E4%B8%BB%E9%A2%98%E4%B8%8E%E6%A0%B7%E5%BC%8F)
13. [综合实战：新闻阅读 App](#%E7%BB%BC%E5%90%88%E5%AE%9E%E6%88%98%EF%BC%9A%E6%96%B0%E9%97%BB%E9%98%85%E8%AF%BB-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` 检查环境

```bash
# 检查环境
flutter doctor

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

```

### macOS

```bash
# 使用 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]` 表示有问题需要修复。

---

## 项目结构

```bash
# 创建新项目
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

```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

```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，清空状态，比冷启动快 |
| 冷启动              | 重新运行                 | 完整编译运行                |

热重载的限制：

- 修改 `initState`、`dispose` 等生命周期方法后需要热重启
- 修改 `main()` 函数后需要热重启
- 修改原生代码（android/ios 目录）后需要冷启动

---

## Widget 核心概念

### 一切皆 Widget

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

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

```

### StatelessWidget vs StatefulWidget

| 对比项  | StatelessWidget | StatefulWidget        |
| ---- | --------------- | --------------------- |
| 状态   | 无内部状态           | 有内部状态（State 对象）       |
| 重建时机 | 父 Widget 重建时    | 父重建 或 调用 setState() 时 |
| 使用场景 | 纯展示，数据来自外部      | 需要响应用户交互、维护内部数据       |
| 性能   | 稍好              | 稍差，但影响可忽略             |

```dart
// StatelessWidget 示例
class GreetingCard extends StatelessWidget {
  final String name; // 数据从外部传入

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

  @override
  Widget build(BuildContext context) {
    return Text('你好，$name');
  }
}

```

```dart
// 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

```dart
@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?            | \-  | 变换矩阵                 |

```dart
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  | 所有空白均匀分配            |

```dart
// 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? | \-  | 高度       |

```dart
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                        |

```dart
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 大小。

```dart
// 固定尺寸
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

```dart
// 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  |

```dart
// 标签流式布局
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\~w900，bold \= 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>?   | 文字阴影                               |

```dart
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

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

```dart
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 | 仅缩小，不放大          |

```dart
// 本地资源图片（需在 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

```dart
// 使用 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?>               | 固定尺寸       |

```dart
// 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? | 内容内边距       |

```dart
// 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    | 是否根据内容计算高度 |

```dart
// 静态列表（数据量少时使用）
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） |

```dart
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 中使其可滚动。

```dart
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                 | 是否自动添加返回按钮             |

```dart
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

```dart
// 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

```dart
Scaffold(
  appBar: AppBar(title: const Text('首页')),
  drawer: Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: [
        // 抽屉头部
        UserAccountsDrawerHeader(
          accountName: const Text('张三'),
          accountEmail: const Text('zhangsan@example.com'),
          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 基础用法

```dart
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`（防止组件已销毁）

```dart
Future<void> _loadData() async {
  final data = await fetchDataFromNetwork();
  // 异步完成后检查组件是否还在树中
  if (!mounted) return;
  setState(() {
    _data = data;
  });
}

```

---

### InheritedWidget 原理

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

```dart
// 简单示例（理解原理，实际使用 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 是官方推荐的轻量级状态管理方案。

```bash
# 添加依赖
flutter pub add provider

```

#### 核心 API 参数表

| 类/方法                   | 参数                                | 说明                      |
| ---------------------- | --------------------------------- | ----------------------- |
| ChangeNotifierProvider | create：(context) => T             | 创建 ChangeNotifier 实例    |
| ChangeNotifierProvider | child：Widget                      | 子 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 的某个字段变化           |

```dart
// 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 的一些限制，是目前最受推荐的状态管理方案之一。

```bash
flutter pub add flutter_riverpod

```

```dart
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 / pop

#### 参数表

| 方法                           | 参数                        | 说明          |
| ---------------------------- | ------------------------- | ----------- |
| 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） |

```dart
// 基础跳转
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 表示清空所有
);

```

---

### 命名路由

```dart
// 在 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 路由、嵌套路由、重定向等。

```bash
flutter pub add go_router

```

```dart
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 包

```bash
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 请求 |

```dart
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 等。

```bash
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） |

```dart
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 等）。

```bash
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               | 是否包含指定键 |

```dart
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 基础

适合存储结构化数据。

```bash
flutter pub add sqflite path

```

```dart
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 表单验证

```dart
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     | 完成            |

```dart
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、实时数据库等）。

```dart
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 |

```dart
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 会自动对属性变化进行动画过渡。

```dart
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 实现两个页面之间的共享元素过渡动画。

```dart
// 第一个页面
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

```dart
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 背景色        |

```dart
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 中读取主题：

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

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

```

---

### 响应式布局

#### MediaQuery

```dart
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 更准确。

```dart
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

```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

```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

```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

```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

```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

```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

```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

```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

```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 在异步回调中的空安全问题

```dart
// 错误：组件已销毁后仍调用 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 中创建对象

```dart
// 错误：每次 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

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

```

### ListView 在 Column 中的无界高度问题

```dart
// 错误：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` 包：

```bash
flutter pub add cached_network_image

```

```dart
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 会跳过这些节点的重建，是最简单的性能优化。

```dart
// 好：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完全指南](https://blog.vercanti.com/gorouter-wan-quan-zhi-nan/)。

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

```dart
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`。

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

```

### 陷阱：`setState` 在 `dispose` 后调用

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

```dart
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入门](https://blog.vercanti.com/dart-ru-men/)  
[Flutter状态管理](https://blog.vercanti.com/flutter-zhuang-tai-guan-li/)  
[GoRouter完全指南](https://blog.vercanti.com/gorouter-wan-quan-zhi-nan/)