← 返回学习路线
🚀

Flutter 基础入门

从零开始掌握 Flutter 框架的核心基础,包括环境搭建、项目结构、Widget 体系和应用构建全流程

📦 第一章:环境搭建

1.1 安装 Flutter SDK

Flutter SDK 是开发 Flutter 应用的核心工具包,包含 Dart SDK、Flutter 框架和各种命令行工具。

🍎 macOS

  • 下载 Flutter SDK 压缩包
  • 解压到 ~/development/flutter
  • 添加到 PATH 环境变量
  • 安装 Xcode 和 CocoaPods

🪟 Windows

  • 下载 Flutter SDK zip 包
  • 解压到 C:\flutter
  • 添加到系统环境变量 PATH
  • 安装 Android Studio

🐧 Linux

  • 下载 Flutter SDK tar 包
  • 解压到 ~/development/flutter
  • .bashrc 中添加 PATH
  • 安装依赖库和 Android Studio
bash
# macOS / Linux 安装步骤
cd ~/development
git clone https://github.com/flutter/flutter.git -b stable

# 添加到 PATH(在 ~/.zshrc 或 ~/.bashrc 中)
export PATH="$HOME/development/flutter/bin:$PATH"

# 使配置生效
source ~/.zshrc

# 验证安装
flutter --version
# Flutter 3.x.x • channel stable
# Dart SDK version: 3.x.x

1.2 使用 FVM 版本管理器(推荐)

FVM(Flutter Version Management)允许你在不同项目中使用不同版本的 Flutter SDK,非常适合团队协作和维护多个项目。

bash
# 安装 FVM
dart pub global activate fvm

# 安装指定版本的 Flutter
fvm install 3.22.0

# 在项目中使用指定版本
fvm use 3.22.0

# 查看已安装的版本列表
fvm list

# 使用 fvm 运行 Flutter 命令
fvm flutter run
fvm flutter build apk
提示:使用 FVM 后,建议在项目的 .gitignore 中添加 .fvm/flutter_sdk,并在 VS Code 的 settings.json 中配置 "dart.flutterSdkPath": ".fvm/flutter_sdk"

1.3 flutter doctor 检查环境

flutter doctor 会检查你的开发环境是否完整,并给出修复建议。

bash
flutter doctor

# 输出示例:
# [✓] Flutter (Channel stable, 3.22.0)
#     Flutter SDK 版本和渠道信息
# [✓] Android toolchain - develop for Android devices
#     Android SDK 路径、版本、构建工具
# [✓] Xcode - develop for iOS and macOS
#     Xcode 版本、CocoaPods 状态
# [✓] Chrome - develop for the web
#     Chrome 浏览器路径
# [✓] Android Studio
#     Android Studio 版本、Flutter/Dart 插件
# [✓] VS Code
#     VS Code 版本、Flutter 扩展
# [✓] Connected device
#     已连接的设备列表

# 查看更详细的信息
flutter doctor -v

1.4 IDE 设置

VS Code(推荐)

  • Flutter 扩展(包含 Dart)
  • Dart 扩展(语法高亮、补全)
  • Awesome Flutter Snippets 代码片段
  • Flutter Widget Snippets
  • Error Lens 行内错误提示

Android Studio

  • Flutter 插件
  • Dart 插件
  • 内置模拟器管理器
  • 性能分析工具
  • 布局检查器

1.5 模拟器 / 真机设置

开发时需要至少一个运行设备,可以使用模拟器或连接真机。

bash
# iOS 模拟器(仅 macOS)
open -a Simulator

# Android 模拟器 —— 通过 Android Studio 的 AVD Manager 创建
# 或使用命令行:
flutter emulators
flutter emulators --launch <emulator_id>

# 查看已连接的设备
flutter devices

# 在指定设备上运行
flutter run -d <device_id>
flutter run -d chrome      # 在 Chrome 中运行
flutter run -d macos       # 作为 macOS 桌面应用运行

1.6 创建并运行第一个项目

bash
# 创建新项目
flutter create my_first_app

# 进入项目目录
cd my_first_app

# 运行项目
flutter run

# 创建项目时指定组织名(影响包名)
flutter create --org com.example my_app

# 指定平台
flutter create --platforms=android,ios,web my_app

1.7 Hot Reload vs Hot Restart

特性 Hot Reload(热重载) Hot Restart(热重启)
快捷键 r 或保存文件 R
速度 亚秒级(通常 < 1s) 数秒(需重新执行 main)
状态保留 保留应用状态(State 不丢失) 重置所有状态
原理 注入更新后的代码到 Dart VM 销毁并重建整个 Widget 树
适用场景 修改 UI、调整样式 修改 initState、全局变量、main()
限制 不能处理静态字段、枚举变更 无限制,但更慢
注意:当 Hot Reload 没有生效时(比如修改了 main() 函数或全局状态),需要使用 Hot Restart。如果连 Hot Restart 也不行,就需要完全重新运行(flutter run)。

📂 第二章:项目结构详解

2.1 项目目录结构

一个标准的 Flutter 项目包含以下目录和文件:

项目结构
my_flutter_app/
├── lib/                    // 主要的 Dart 源代码目录
│   └── main.dart           // 应用入口文件,包含 main() 函数
├── test/                   // 单元测试和 Widget 测试
│   └── widget_test.dart     // 默认的 Widget 测试文件
├── android/                // Android 平台原生代码和配置
│   ├── app/
│   │   ├── build.gradle     // 应用级别的 Gradle 配置
│   │   └── src/main/
│   │       └── AndroidManifest.xml
│   └── build.gradle         // 项目级别的 Gradle 配置
├── ios/                    // iOS 平台原生代码和配置
│   ├── Runner/
│   │   ├── Info.plist       // iOS 应用配置
│   │   └── AppDelegate.swift
│   └── Runner.xcworkspace
├── web/                    // Web 平台文件
│   └── index.html
├── build/                  // 构建输出目录(自动生成,不要提交到 Git)
├── .dart_tool/             // Dart 工具缓存(自动生成)
├── .packages               // 包路径映射(已弃用,由 package_config.json 替代)
├── pubspec.yaml            // 项目配置文件(依赖、资源、字体等)
├── pubspec.lock             // 锁定的依赖版本
├── analysis_options.yaml    // Dart 分析器配置
└── README.md                // 项目说明文档

2.2 pubspec.yaml 详解

pubspec.yaml 是 Flutter 项目的核心配置文件,用于声明项目信息、依赖和资源。

pubspec.yaml
name: my_flutter_app              # 项目名称(必须为小写加下划线)
description: 一个全新的 Flutter 项目  # 项目描述
version: 1.0.0+1                 # 版本号 + 构建号
publish_to: 'none'                # 不发布到 pub.dev

# Dart SDK 版本约束
environment:
  sdk: '>=3.0.0 <4.0.0'

# 项目依赖(运行时需要)
dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0                   # ^表示兼容版本(>=1.1.0 <2.0.0)
  provider: ^6.0.5               # 状态管理
  shared_preferences: ^2.2.0    # 本地存储
  intl: any                       # any 表示任意版本(不推荐)
  my_package:                     # Git 依赖
    git:
      url: https://github.com/user/repo.git
      ref: main

# 开发依赖(仅开发和测试时需要)
dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0
  build_runner: ^2.4.0
  mockito: ^5.4.0

# Flutter 特有配置
flutter:
  uses-material-design: true    # 启用 Material Design 图标

  # 静态资源声明
  assets:
    - assets/images/               # 整个目录
    - assets/data/config.json      # 单个文件

  # 自定义字体
  fonts:
    - family: CustomFont
      fonts:
        - asset: assets/fonts/CustomFont-Regular.ttf
        - asset: assets/fonts/CustomFont-Bold.ttf
          weight: 700

2.3 包管理命令

bash
# 获取依赖(根据 pubspec.yaml 下载包)
flutter pub get

# 升级依赖到最新兼容版本
flutter pub upgrade

# 升级指定包
flutter pub upgrade http

# 查看可升级的包
flutter pub outdated

# 添加依赖
flutter pub add provider
flutter pub add --dev build_runner  # 添加到 dev_dependencies

# 移除依赖
flutter pub remove provider

版本约束语法

语法 含义 示例
^1.2.3 兼容版本,等价于 >=1.2.3 <2.0.0 最常用的约束方式
>=1.0.0 <2.0.0 明确的版本范围 精确控制版本
any 任意版本 不推荐,可能导致兼容问题
1.2.3 锁定精确版本 特殊场景下使用

2.4 pub.dev 使用

pub.dev 是 Dart 和 Flutter 的官方包仓库。选择包时请关注以下指标:

  • Likes — 社区喜爱程度
  • Pub Points — 代码质量评分(满分 160)
  • Popularity — 使用广泛程度
  • Flutter Favorite — Flutter 官方推荐标识
  • 平台兼容性 — 支持 Android、iOS、Web、Desktop 等

🧩 第三章:Widget 核心概念

3.1 一切皆 Widget

在 Flutter 中,一切都是 Widget。按钮是 Widget,文本是 Widget,间距是 Widget,甚至整个应用也是一个 Widget。Widget 是 Flutter UI 的基本构建块,它描述了在给定配置和状态下视图应该长什么样。

核心理念:Widget 是不可变的(immutable)。当 UI 需要更新时,Flutter 不会修改现有的 Widget,而是创建一个新的 Widget 来替代。

3.2 三棵树详解

Flutter 框架内部维护三棵关键的树结构,理解它们对于掌握 Flutter 的渲染机制至关重要:

🌳 Widget Tree

配置信息树。由开发者编写的 Widget 代码构成,描述 UI 的结构和配置。Widget 是不可变的、轻量的,频繁重建开销很小。

🌲 Element Tree

生命周期管理树。Element 是 Widget 的实例化对象,管理 Widget 和 RenderObject 之间的关联。Element 是可变的,会被尽量复用。

🎄 RenderObject Tree

布局和绘制树。负责实际的布局计算(大小、位置)和绘制(将像素画到屏幕上)。这是最"重"的树,创建代价高。

渲染流程:Widget(描述配置)→ Element(管理生命周期,diff 算法比较)→ RenderObject(布局、绘制)→ 屏幕像素

渲染流程示意
// Widget 树 → Element 树 → RenderObject 树
//
// MaterialApp           MaterialApp Element        (无 RenderObject)
//   └─ Scaffold          └─ Scaffold Element       RenderFlex
//       └─ Center            └─ Center Element      RenderPositionedBox
//           └─ Text              └─ Text Element    RenderParagraph
//
// 当 Widget 重建时:
// 1. 新 Widget 与旧 Widget 比较(canUpdate: runtimeType + key)
// 2. 如果可以更新 → 复用 Element,更新 RenderObject
// 3. 如果不能更新 → 卸载旧 Element,创建新 Element 和 RenderObject

3.3 StatelessWidget — 无状态组件

StatelessWidget 用于不需要维护内部状态的 UI 组件。一旦创建,其显示内容完全由传入的参数决定。

Dart
import 'package:flutter/material.dart';

// 自定义的无状态组件 —— 用户信息卡片
class UserCard extends StatelessWidget {
  // 通过构造函数接收数据(不可变)
  final String name;
  final String email;
  final String avatarUrl;

  // 使用 const 构造函数提升性能
  const UserCard({
    super.key,
    required this.name,
    required this.email,
    required this.avatarUrl,
  });

  // build 方法 —— 描述该组件的 UI
  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            // 头像
            CircleAvatar(
              radius: 30,
              backgroundImage: NetworkImage(avatarUrl),
            ),
            const SizedBox(width: 16),
            // 用户信息
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  name,
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                Text(
                  email,
                  style: TextStyle(color: Colors.grey[600]),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// 使用方式
const UserCard(
  name: '张三',
  email: 'zhangsan@example.com',
  avatarUrl: 'https://example.com/avatar.png',
)

3.4 StatefulWidget — 有状态组件

StatefulWidget 用于需要维护和管理内部可变状态的组件。它由两个类组成:Widget 类(不可变)和 State 类(可变)。

Dart
import 'package:flutter/material.dart';

// 有状态组件 —— 点赞按钮
class LikeButton extends StatefulWidget {
  final int initialCount;

  const LikeButton({super.key, this.initialCount = 0});

  @override
  State<LikeButton> createState() => _LikeButtonState();
}

class _LikeButtonState extends State<LikeButton> {
  // 可变状态
  late int _count;
  bool _isLiked = false;

  @override
  void initState() {
    super.initState();
    // 从 Widget 属性初始化状态
    _count = widget.initialCount;
  }

  void _toggleLike() {
    // setState 通知框架状态已改变,需要重建 UI
    setState(() {
      _isLiked = !_isLiked;
      _count += _isLiked ? 1 : -1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        IconButton(
          icon: Icon(
            _isLiked ? Icons.favorite : Icons.favorite_border,
            color: _isLiked ? Colors.red : Colors.grey,
          ),
          onPressed: _toggleLike,
        ),
        Text('$_count'),
      ],
    );
  }
}

3.5 Widget 生命周期

StatefulWidget 有完整的生命周期,理解每个阶段对于正确管理资源非常重要:

方法 调用时机 典型用途
createState() Widget 第一次插入树时 创建 State 对象
initState() State 对象创建后,仅调用一次 初始化数据、订阅、控制器
didChangeDependencies() 依赖(如 InheritedWidget)变化时 获取 Theme、MediaQuery 等
build() 每次需要渲染 UI 时 构建 Widget 树(必须是纯函数)
didUpdateWidget() 父 Widget 重建,传入新配置时 对比新旧 Widget,更新状态
deactivate() State 从树中移除时 清理与树相关的引用
dispose() State 永久销毁时 释放资源:controller、subscription

生命周期流程:

生命周期
// 创建阶段:
//   构造函数 → createState() → initState() → didChangeDependencies() → build()
//
// 更新阶段(setState 或父 Widget 重建):
//   didUpdateWidget() → build()
//   setState() → build()
//
// 依赖变化(InheritedWidget 更新):
//   didChangeDependencies() → build()
//
// 销毁阶段:
//   deactivate() → dispose()

3.6 完整生命周期代码示例

Dart
import 'package:flutter/material.dart';

class LifecycleDemo extends StatefulWidget {
  final String title;

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

  @override
  State<LifecycleDemo> createState() {
    print('1. createState —— 创建 State 对象');
    return _LifecycleDemoState();
  }
}

class _LifecycleDemoState extends State<LifecycleDemo> {
  late int _counter;

  @override
  void initState() {
    super.initState();
    _counter = 0;
    print('2. initState —— 初始化状态,仅调用一次');
    // 适合在此处:初始化控制器、订阅流、发起网络请求
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('3. didChangeDependencies —— 依赖变化');
    // 可以安全使用 context 获取 InheritedWidget
    // 例如:Theme.of(context), MediaQuery.of(context)
  }

  @override
  Widget build(BuildContext context) {
    print('4. build —— 构建 UI');
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('${widget.title}: $_counter'),
        ElevatedButton(
          onPressed: () {
            setState(() {
              _counter++;
              print('setState —— 触发重建');
            });
          },
          child: const Text('增加'),
        ),
      ],
    );
  }

  @override
  void didUpdateWidget(LifecycleDemo oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('5. didUpdateWidget —— 父 Widget 传入新配置');
    if (oldWidget.title != widget.title) {
      print('   title 从 "${oldWidget.title}" 变为 "${widget.title}"');
    }
  }

  @override
  void deactivate() {
    print('6. deactivate —— 从 Widget 树中暂时移除');
    super.deactivate();
  }

  @override
  void dispose() {
    print('7. dispose —— 永久销毁,释放资源');
    // 在此处释放:controller.dispose(), subscription.cancel()
    super.dispose();
  }
}

🔍 第四章:BuildContext

4.1 什么是 BuildContext

BuildContext 是 Widget 在 Widget 树中位置的一个引用句柄。实际上,每个 BuildContext 就是对应 Element 树中的一个 Element 对象。它用于向上查找祖先 Widget 提供的数据和服务。

Dart
// BuildContext 本质上就是 Element
// abstract class Element implements BuildContext { ... }

// 在 build 方法中,context 代表当前 Widget 在树中的位置
@override
Widget build(BuildContext context) {
  // context 可以向上查找祖先提供的数据
  return Container();
}

4.2 BuildContext 与 Element 树的关系

每个 Widget 的 build 方法接收的 context 参数,代表的是该 Widget 在 Element 树中对应 Element 的引用。通过这个引用,可以沿树向上查找祖先节点提供的信息。

4.3 使用 context 查找祖先数据

Dart
@override
Widget build(BuildContext context) {
  // 获取当前主题数据
  final theme = Theme.of(context);
  final primaryColor = theme.colorScheme.primary;

  // 获取屏幕尺寸信息
  final mediaQuery = MediaQuery.of(context);
  final screenWidth = mediaQuery.size.width;
  final isLandscape = mediaQuery.orientation == Orientation.landscape;

  // 获取 Navigator(用于页面导航)
  Navigator.of(context).push(...);

  // 获取 Scaffold 的状态(如打开 Drawer)
  Scaffold.of(context).openDrawer();

  // 查找最近的指定类型祖先 Widget
  final scaffold = context.findAncestorWidgetOfExactType<Scaffold>();

  // 查找最近的指定类型祖先 State
  final state = context.findAncestorStateOfType<ScaffoldState>();

  return Text('屏幕宽度: $screenWidth');
}

4.4 Context 与 Widget 重建

当使用 of(context) 方法查找 InheritedWidget 时,当前 Widget 会自动注册为该 InheritedWidget 的依赖者。当 InheritedWidget 数据变化时,所有依赖它的 Widget 都会被重建。

Dart
// 这会注册依赖关系 —— Theme 变化时此 Widget 会重建
final theme = Theme.of(context);

// 如果只想读取一次,不注册依赖(不会自动重建)
// 适合在 didChangeDependencies 中使用
final route = ModalRoute.of(context);

4.5 常见错误

错误 1:在错误的 context 中查找
Scaffold 的构建方法内部使用 Scaffold.of(context) 会失败,因为 context 指向的 Element 在 Scaffold 的上方,而不是下方。
Dart
// ❌ 错误用法 —— context 在 Scaffold 上方
class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ElevatedButton(
        onPressed: () {
          // 这里的 context 是 MyPage 的,在 Scaffold 之上
          // Scaffold.of(context) 找不到 Scaffold!
          Scaffold.of(context).openDrawer();  // 报错!
        },
        child: const Text('打开抽屉'),
      ),
    );
  }
}

// ✅ 正确用法 —— 使用 Builder 获取 Scaffold 下方的 context
class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: const Drawer(child: Text('抽屉内容')),
      body: Builder(
        builder: (BuildContext innerContext) {
          // innerContext 在 Scaffold 下方,可以正确找到
          return ElevatedButton(
            onPressed: () {
              Scaffold.of(innerContext).openDrawer();  // 正确!
            },
            child: const Text('打开抽屉'),
          );
        },
      ),
    );
  }
}
错误 2:在 initState 中使用 context 访问 InheritedWidget
initState 执行时,Widget 还没有完全挂载到树中,此时通过 context 查找 InheritedWidget 可能不可靠。应该在 didChangeDependencies 中进行。
Dart
// ❌ 不推荐 —— initState 中使用 context 获取 InheritedWidget
@override
void initState() {
  super.initState();
  final theme = Theme.of(context);  // 可能有问题
}

// ✅ 推荐 —— 在 didChangeDependencies 中使用
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  final theme = Theme.of(context);  // 安全
}

🔑 第五章:Key 的使用

5.1 为什么 Key 很重要

Flutter 通过比较 Widget 的 runtimeTypekey 来决定是否复用 Element。在列表重新排序、添加/删除元素等场景中,如果没有正确使用 Key,Flutter 可能会错误地复用 Element,导致状态混乱。

5.2 Key 的类型

Key 类型 说明 使用场景
ValueKey 基于一个值(如 ID、字符串)生成 Key 列表项有唯一标识符时
ObjectKey 基于对象的引用地址生成 Key 用对象本身作为唯一标识
UniqueKey 每次创建都生成唯一的 Key 强制重建 Widget(不复用)
GlobalKey 全局唯一,可跨 Widget 树访问 State 访问子 Widget 的 State、表单验证

5.3 何时使用 Key

Dart
// 场景 1:列表重新排序 —— 必须使用 Key
ListView(
  children: todos.map((todo) =>
    TodoTile(
      key: ValueKey(todo.id),  // 使用唯一 ID 作为 Key
      todo: todo,
    ),
  ).toList(),
)

// 场景 2:条件渲染不同类型的 Widget —— 使用 ValueKey 区分
AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  child: isLoggedIn
    ? UserProfile(key: const ValueKey('profile'))
    : LoginForm(key: const ValueKey('login')),
)

// 场景 3:强制重建(状态重置)—— 使用 UniqueKey
MyWidget(key: UniqueKey())  // 每次都生成新 Key,强制重建

5.4 GlobalKey 跨 Widget 访问 State

Dart
import 'package:flutter/material.dart';

// 使用 GlobalKey 访问子 Widget 的 State
class ParentWidget extends StatefulWidget {
  const ParentWidget({super.key});

  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  // 创建 GlobalKey,泛型为子 Widget 的 State 类型
  final _counterKey = GlobalKey<_CounterWidgetState>();
  // 表单的 GlobalKey
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 传递 GlobalKey 给子 Widget
        CounterWidget(key: _counterKey),
        ElevatedButton(
          onPressed: () {
            // 通过 GlobalKey 访问子 Widget 的 State
            _counterKey.currentState?.increment();
          },
          child: const Text('从父 Widget 增加计数'),
        ),
        // 表单验证示例
        Form(
          key: _formKey,
          child: TextFormField(
            validator: (value) =>
              value?.isEmpty ?? true ? '请输入内容' : null,
          ),
        ),
        ElevatedButton(
          onPressed: () {
            // 通过 GlobalKey 验证表单
            if (_formKey.currentState!.validate()) {
              print('表单验证通过');
            }
          },
          child: const Text('提交'),
        ),
      ],
    );
  }
}

// 子 Widget —— 带有公开方法的 StatefulWidget
class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  // 公开方法,可通过 GlobalKey 从外部调用
  void increment() {
    setState(() => _count++);
  }

  @override
  Widget build(BuildContext context) {
    return Text('计数: $_count', style: const TextStyle(fontSize: 24));
  }
}
注意:GlobalKey 开销较大,不应滥用。大多数情况下优先考虑使用回调函数或状态管理方案来实现组件间通信,而非 GlobalKey。

🏗️ 第六章:MaterialApp 与 Scaffold

6.1 MaterialApp 配置

MaterialApp 是 Material Design 应用的顶层 Widget,负责全局配置,包括主题、路由、本地化等。

Dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 应用标题(在任务切换器中显示)
      title: '我的 Flutter 应用',

      // 隐藏右上角 DEBUG 标识
      debugShowCheckedModeBanner: false,

      // 全局主题配置
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),

      // 深色主题
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),

      // 跟随系统切换明暗主题
      themeMode: ThemeMode.system,

      // 首页
      home: const HomePage(),

      // 命名路由表
      routes: {
        '/settings': (context) => const SettingsPage(),
        '/profile': (context) => const ProfilePage(),
      },
    );
  }
}

6.2 ThemeData 自定义

Dart
// Material 3 推荐的主题配置方式
final theme = ThemeData(
  // 使用种子颜色自动生成配色方案
  colorScheme: ColorScheme.fromSeed(
    seedColor: const Color(0xFF027DFD),
  ),
  useMaterial3: true,

  // 自定义文字主题
  textTheme: const TextTheme(
    headlineLarge: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
    bodyLarge: TextStyle(fontSize: 16),
    bodyMedium: TextStyle(fontSize: 14),
  ),

  // 自定义 AppBar 主题
  appBarTheme: const AppBarTheme(
    centerTitle: true,
    elevation: 0,
  ),

  // 自定义卡片主题
  cardTheme: CardTheme(
    elevation: 2,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),

  // 自定义按钮主题
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(
      padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      ),
    ),
  ),
);

6.3 Scaffold 结构

Scaffold 提供了 Material Design 的基本页面布局结构,包括顶栏、底栏、浮动按钮、抽屉等。

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

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

class _HomePageState extends State<HomePage> {
  int _currentIndex = 0;

  final List<Widget> _pages = [
    const Center(child: Text('首页内容')),
    const Center(child: Text('搜索页面')),
    const Center(child: Text('个人中心')),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 顶部导航栏
      appBar: AppBar(
        title: const Text('我的应用'),
        centerTitle: true,
        actions: [
          IconButton(
            icon: const Icon(Icons.notifications),
            onPressed: () {},
          ),
          IconButton(
            icon: const Icon(Icons.settings),
            onPressed: () {
              Navigator.pushNamed(context, '/settings');
            },
          ),
        ],
      ),

      // 左侧抽屉菜单
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            const DrawerHeader(
              decoration: BoxDecoration(color: Colors.blue),
              child: Text('菜单',
                style: TextStyle(color: Colors.white, fontSize: 24)),
            ),
            ListTile(
              leading: const Icon(Icons.home),
              title: const Text('首页'),
              onTap: () => Navigator.pop(context),
            ),
            ListTile(
              leading: const Icon(Icons.person),
              title: const Text('个人资料'),
              onTap: () {},
            ),
          ],
        ),
      ),

      // 页面主体内容
      body: _pages[_currentIndex],

      // 浮动操作按钮
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          print('点击了 FAB');
        },
        child: const Icon(Icons.add),
      ),

      // 底部导航栏
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (int index) {
          setState(() => _currentIndex = index);
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: '首页',
          ),
          NavigationDestination(
            icon: Icon(Icons.search),
            selectedIcon: Icon(Icons.search),
            label: '搜索',
          ),
          NavigationDestination(
            icon: Icon(Icons.person_outline),
            selectedIcon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

📱 第七章:第一个完整 App

7.1 构建一个增强版计数器应用

我们将在默认计数器的基础上,添加自定义样式、多种操作和简单的多页面概念,构建一个完整的学习示例。

第一步:创建项目

bash
flutter create enhanced_counter
cd enhanced_counter
flutter run

第二步:编写完整代码

lib/main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const EnhancedCounterApp());
}

// 应用根组件
class EnhancedCounterApp extends StatelessWidget {
  const EnhancedCounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '增强计数器',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4),
        ),
        useMaterial3: true,
      ),
      home: const CounterHomePage(),
      routes: {
        '/history': (context) => const HistoryPage(),
      },
    );
  }
}

// 主页 —— 有状态组件
class CounterHomePage extends StatefulWidget {
  const CounterHomePage({super.key});

  @override
  State<CounterHomePage> createState() => _CounterHomePageState();
}

class _CounterHomePageState extends State<CounterHomePage> {
  int _counter = 0;
  final List<String> _history = [];  // 操作历史

  // 增加计数
  void _increment() {
    setState(() {
      _counter++;
      _history.add('增加到 $_counter');
    });
  }

  // 减少计数
  void _decrement() {
    setState(() {
      _counter--;
      _history.add('减少到 $_counter');
    });
  }

  // 重置计数
  void _reset() {
    setState(() {
      _counter = 0;
      _history.add('重置为 0');
    });
  }

  // 根据计数值返回不同颜色
  Color _getCounterColor() {
    if (_counter > 0) return Colors.green;
    if (_counter < 0) return Colors.red;
    return Colors.grey;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('增强计数器'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          // 跳转到历史页面
          IconButton(
            icon: const Icon(Icons.history),
            onPressed: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => HistoryPage(history: _history),
                ),
              );
            },
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 提示文字
            const Text('当前计数值', style: TextStyle(fontSize: 16)),
            const SizedBox(height: 8),
            // 计数显示 —— 带动画颜色变化
            AnimatedDefaultTextStyle(
              duration: const Duration(milliseconds: 300),
              style: TextStyle(
                fontSize: 72,
                fontWeight: FontWeight.bold,
                color: _getCounterColor(),
              ),
              child: Text('$_counter'),
            ),
            const SizedBox(height: 32),
            // 操作按钮行
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // 减少按钮
                FilledButton.tonal(
                  onPressed: _decrement,
                  child: const Icon(Icons.remove),
                ),
                const SizedBox(width: 16),
                // 重置按钮
                OutlinedButton(
                  onPressed: _reset,
                  child: const Text('重置'),
                ),
                const SizedBox(width: 16),
                // 增加按钮
                FilledButton(
                  onPressed: _increment,
                  child: const Icon(Icons.add),
                ),
              ],
            ),
          ],
        ),
      ),
      // 浮动按钮也可以增加
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _increment,
        label: const Text('增加'),
        icon: const Icon(Icons.add),
      ),
    );
  }
}

// 历史记录页面 —— 展示多页面跳转
class HistoryPage extends StatelessWidget {
  final List<String> history;

  const HistoryPage({super.key, this.history = const []});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('操作历史')),
      body: history.isEmpty
        ? const Center(child: Text('暂无操作记录'))
        : ListView.builder(
            itemCount: history.length,
            itemBuilder: (context, index) {
              // 倒序显示,最新的在最上面
              final item = history[history.length - 1 - index];
              return ListTile(
                leading: CircleAvatar(child: Text('${history.length - index}')),
                title: Text(item),
              );
            },
          ),
    );
  }
}

7.2 在设备上运行

bash
# 查看可用设备
flutter devices

# 在指定设备上运行
flutter run -d <device_id>

# 构建发布版本
flutter build apk          # Android APK
flutter build ios          # iOS(需要 macOS + Xcode)
flutter build web          # Web

7.3 使用 DevTools 调试

Flutter DevTools 是一套强大的性能和调试工具套件。

bash
# 在应用运行时,打开 DevTools
flutter run
# 运行后在终端按 D 打开 DevTools

# 或者直接启动
dart devtools

Widget Inspector

可视化查看 Widget 树结构,检查每个 Widget 的属性、约束和大小。类似浏览器的 Elements 面板。

Performance

查看帧率图表,识别 UI 卡顿(jank)。帮助发现不必要的重建和耗时操作。

Network

监控网络请求,查看请求和响应详情。调试 API 调用问题。

Logging

查看应用日志输出,包括 print 语句和框架日志。支持过滤和搜索。

调试技巧:在运行模式下按 p 可以显示性能覆盖层(Performance Overlay),直观查看 UI 线程和 GPU 线程的帧渲染时间。按 i 可以切换 Widget Inspector。

✏️ 第八章:实践练习

练习 1:个人名片 App

创建一个展示个人信息的应用,练习 StatelessWidget 和基本布局。

  1. 使用 flutter create 创建新项目
  2. 设计一个个人名片界面,包含头像(CircleAvatar)、姓名、职业、联系方式
  3. 使用 CardColumnRowIcon 等 Widget 组织布局
  4. 通过 ThemeData 自定义应用主题颜色
  5. 确保界面在不同屏幕尺寸下合理显示

练习 2:温度转换器

构建一个摄氏度和华氏度互相转换的工具,练习 StatefulWidget 和用户输入。

  1. 使用 TextField 接收用户输入的温度值
  2. 使用 StatefulWidget 管理输入状态和转换结果
  3. 实现摄氏度 ↔ 华氏度的双向转换(公式:°F = °C × 9/5 + 32)
  4. 添加一个切换按钮,切换转换方向
  5. 使用 TextEditingController 管理输入框,并在 dispose 中释放

练习 3:待办清单(带生命周期日志)

构建一个简单的待办事项清单,练习 Key 的使用和生命周期管理。

  1. 实现添加、删除、标记完成功能
  2. 使用 ListView.builder 展示列表,为每个项目添加 ValueKey
  3. 支持拖拽排序(ReorderableListView),观察 Key 的作用
  4. 在 StatefulWidget 的每个生命周期方法中添加 print,观察调用顺序
  5. 添加一个 Drawer,使用 Navigator 实现从 Drawer 跳转到"关于"页面