这篇文章记录一下我最近学习和开发 Flutter 时走过的完整流程,适合第一次接触 Flutter 的开发者。目标不是一次讲完所有知识,而是从安装环境开始,最后真正跑出一个可以新增、完成和删除任务的待办应用。

教程默认使用 Windows,编辑器使用 VS Code,运行设备使用 Android 模拟器。第一次配置时遇到报错很正常,首次搭建 Flutter 开发环境时,问题往往集中在 SDK、环境变量、Android 工具链和模拟器配置,而不一定是代码本身。

1. 先认识一下 Flutter

Flutter 是一个跨平台 UI 框架,使用 Dart 语言开发。一份代码可以运行在 Android、iOS、Web、Windows、macOS 和 Linux 上,不过不同平台仍然需要各自的开发工具。其中,iOS 和 macOS 开发需要使用 macOS,iOS 开发还需要安装 Xcode。

刚开始只需要记住三件事:

  1. Flutter SDK:提供 flutter 命令和框架代码。
  2. Dart SDK:已经包含在 Flutter SDK 里,不需要单独安装。
  3. Android Studio:主要用来提供 Android SDK、模拟器和相关工具。平时写代码可以使用 VS Code。

Flutter 的界面由 Widget 组成。文字、按钮、输入框、间距、页面本身,基本都可以看成 Widget。写页面的过程,就是把这些 Widget 一层一层组合起来。

2. 安装开发环境

2.1 安装 Git

Flutter 在安装和获取依赖时会用到 Git,所以先安装 Git:

Git 官方下载地址

安装时基本一路下一步即可。安装完成后打开 PowerShell,执行:

PowerShell
git --version

能看到版本号就说明 Git 已经安装好了。

2.2 下载 Flutter SDK

打开 Flutter 官方安装页面,下载 Windows 的 stable 版本:

Flutter Windows 安装文档

建议把 SDK 解压到一个路径简单的目录,例如:

Text
C:\src\flutter

不建议放到 C:\Program Files,也不要放在带空格、中文或权限比较严格的目录里。后面安装依赖和编译 Android 项目时,路径问题会让排查变得很麻烦。

解压完成后,Flutter 命令所在的位置是:

Text
C:\src\flutter\bin

2.3 配置环境变量

把下面这个目录加入 Windows 的用户 Path:

Text
C:\src\flutter\bin

配置方法:

  1. 在 Windows 搜索框输入“环境变量”。
  2. 打开“编辑系统环境变量”。
  3. 点击“环境变量”。
  4. 在“用户变量”中找到 Path,点击编辑。
  5. 新增 C:\src\flutter\bin
  6. 一路确定,然后重新打开 PowerShell 和 VS Code。

检查 Flutter 是否能使用:

PowerShell
flutter --version

如果提示找不到 flutter,通常是终端没有重启,或者 Path 填错了。先关闭当前终端,重新打开再试。

2.4 安装 VS Code 和插件

安装 VS Code:

VS Code 官方下载地址

然后在扩展市场安装这两个插件:

  • Flutter
  • Dart

安装 Flutter 插件时,Dart 插件通常会一起安装。打开 VS Code 的命令面板,执行 Flutter: Run Flutter Doctor,也可以帮助检查环境。

2.5 安装 Android Studio 和 Android SDK

即使平时用 VS Code 写代码,也建议安装 Android Studio,因为 Android SDK 和模拟器需要它来管理。

下载地址:

Android Studio 官方下载地址

安装完成后打开 Android Studio,进入 SDK Manager。不同版本的界面文字可能略有区别,重点是确认 Android SDK 和下面这些工具已经安装:

  • Android SDK Platform
  • Android SDK Build-Tools
  • Android SDK Command-line Tools
  • Android SDK Platform-Tools
  • Android Emulator
  • CMake
  • NDK(Side by side)

SDK Manager 一般可以在这里找到:

Text
Android Studio → More Actions → SDK Manager

如果已经打开了一个项目,可以从菜单进入:

Text
Tools → SDK Manager

Flutter 官方的 Android 环境说明会根据当前稳定版本更新具体 API Level,跟着官方页面选择推荐的稳定 SDK 即可,不需要死记某个版本号。

2.6 接受 Android SDK 协议

打开 PowerShell,执行:

PowerShell
flutter doctor --android-licenses

每次出现协议时输入 y 接受。最后看到类似下面的提示,说明协议处理完成:

Text
All SDK package licenses accepted.

2.7 检查环境

执行:

PowerShell
flutter doctor

正常情况下会看到几项绿色的勾。刚安装好时不一定全部通过,先重点看 Android toolchain、Android Studio 和 VS Code 这几项。

还可以执行:

PowerShell
flutter devices

这个命令会列出 Flutter 当前识别到的设备。如果暂时没有设备,不代表 Flutter 安装失败,只是还没有启动模拟器或连接手机。

3. 创建 Android 模拟器

第一次学习建议使用模拟器,省去连接真机和 USB 驱动的麻烦。

打开 Android Studio 的 Device Manager:

Text
More Actions → Virtual Device Manager

如果已经打开项目,可以从这里进入:

Text
Tools → Device Manager

创建模拟器的步骤:

  1. 点击 Create Virtual Device
  2. 选择一个 Phone,例如 Pixel 系列。
  3. 选择一个已经下载好的系统镜像。
  4. 如果没有系统镜像,点击下载,等待安装完成。
  5. 使用默认配置完成创建。
  6. 点击模拟器右侧的启动按钮。

模拟器启动后,在 PowerShell 中执行:

PowerShell
flutter devices

能看到一个 Android 设备就可以继续了。

如果模拟器启动很慢,或者启动后黑屏,先检查 BIOS 中是否开启虚拟化。Windows 也可能需要开启对应的虚拟机平台功能。电脑配置较低时,可以把模拟器分辨率和内存调低一些。

4. 创建第一个 Flutter 项目

先进入一个用来存放项目的目录。例如:

PowerShell
cd D:\flutter-projects

如果目录还不存在,可以先创建:

PowerShell
mkdir D:\flutter-projects
cd D:\flutter-projects

创建项目:

PowerShell
flutter create todo_demo

进入项目目录:

PowerShell
cd todo_demo

查看当前项目能运行在哪些设备上:

PowerShell
flutter devices

启动项目:

PowerShell
flutter run

如果有多个设备,Flutter 会让你选择一个。也可以指定设备运行:

PowerShell
flutter run -d emulator-5554

这里的 emulator-5554 要替换成 flutter devices 显示的设备 ID。

第一次编译 Android 项目可能比较慢,因为 Gradle 需要下载依赖。正常情况下,终端会持续输出下载或构建日志;如果长时间没有新的日志,或者反复出现下载失败,不要一直等待,应根据具体报错检查网络、Gradle 和 Android 工具链。首次构建完成后,之后再次运行通常会快很多。

5. 先看懂项目目录

用 VS Code 打开 todo_demo,先不用急着改代码,简单看一下结构。下面只列出本文会重点接触的主要目录;根据启用的平台不同,项目中还可能包含 windows/macos/linux/ 等目录:

Text
todo_demo/
├─ android/       Android 原生工程配置
├─ ios/           iOS 工程配置
├─ lib/           主要的 Dart 代码
│  └─ main.dart   程序入口
├─ test/          测试代码
├─ web/           Web 平台文件
├─ pubspec.yaml   项目名称、依赖、资源配置
└─ analysis_options.yaml  Dart 静态检查配置

平时最常修改的是两个地方:

  • lib/:写页面和业务代码。
  • pubspec.yaml:添加第三方依赖、图片、字体等资源。

androidios 等目录不是不能改,而是刚开始不要随便修改。很多 Android 编译问题都是改了原生配置后才出现的。

6. 做一个待办事项应用

现在把 lib/main.dart 里面的默认示例全部删除,替换成下面的代码。

这个版本故意没有使用 Provider、Riverpod、Bloc 等状态管理框架。项目还很小,先把 Flutter 最核心的状态变化弄明白更重要。

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

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: '待办事项',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
      ),
      home: const TodoPage(),
    );
  }
}

class TodoItem {
  TodoItem({required this.title});

  final String title;
  bool isDone = false;
}

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

  @override
  State<TodoPage> createState() => _TodoPageState();
}

class _TodoPageState extends State<TodoPage> {
  final TextEditingController _controller = TextEditingController();
  final List<TodoItem> _todos = [];

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _addTodo() {
    final title = _controller.text.trim();

    if (title.isEmpty) {
      return;
    }

    setState(() {
      _todos.add(TodoItem(title: title));
      _controller.clear();
    });
  }

  void _toggleTodo(int index, bool value) {
    setState(() {
      _todos[index].isDone = value;
    });
  }

  void _removeTodo(int index) {
    setState(() {
      _todos.removeAt(index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('我的待办'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _controller,
                    textInputAction: TextInputAction.done,
                    onSubmitted: (_) => _addTodo(),
                    decoration: const InputDecoration(
                      labelText: '准备做什么?',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                FilledButton(
                  onPressed: _addTodo,
                  child: const Text('添加'),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Expanded(
              child: _todos.isEmpty
                  ? const Center(
                      child: Text('还没有待办事项'),
                    )
                  : ListView.separated(
                      itemCount: _todos.length,
                      separatorBuilder: (_, __) => const Divider(height: 1),
                      itemBuilder: (context, index) {
                        final todo = _todos[index];

                        return ListTile(
                          leading: Checkbox(
                            value: todo.isDone,
                            onChanged: (value) {
                              _toggleTodo(index, value ?? false);
                            },
                          ),
                          title: Text(
                            todo.title,
                            style: TextStyle(
                              decoration: todo.isDone
                                  ? TextDecoration.lineThrough
                                  : null,
                              color: todo.isDone ? Colors.grey : null,
                            ),
                          ),
                          trailing: IconButton(
                            tooltip: '删除',
                            onPressed: () => _removeTodo(index),
                            icon: const Icon(Icons.delete_outline),
                          ),
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

保存文件后,如果当前是通过命令行执行 flutter run,默认不会因为保存文件而自动触发 Hot Reload,需要在运行终端中按 r。如果使用 VS Code 的 Flutter 调试功能,则可以通过编辑器触发 Hot Reload,也可以根据 VS Code/Flutter 插件设置配置保存时自动刷新。

现在可以依次测试:

  1. 在输入框里输入一件事情。
  2. 点击“添加”,确认列表里出现任务。
  3. 点击左侧复选框,确认任务变成删除线。
  4. 点击右侧垃圾桶,确认任务被删除。
  5. 不输入内容直接点击“添加”,确认不会添加空任务。

这几个动作虽然简单,但已经覆盖了一个 Flutter 页面最常见的开发流程:读取输入、修改数据、刷新界面、响应点击和渲染列表。

7. 这段代码到底做了什么

7.1 main() 是程序入口,runApp() 挂载根 Widget

DART
void main() {
  runApp(const TodoApp());
}

Dart 程序从 main() 函数开始执行,它才是程序入口。runApp() 会接收一个 Widget,并把它挂载为应用的根 Widget;这里的 TodoApp 就是整个应用的根 Widget。

7.2 StatelessWidget 和 StatefulWidget

TodoApp 使用 StatelessWidget,因为它自身不需要维护可变状态。StatelessWidget 并不代表只会构建一次,在父组件或依赖发生变化时,它仍然可能重新执行 build

TodoPage 使用 StatefulWidget,因为待办列表会变化:添加任务、勾选任务和删除任务都会改变数据。

真正保存变化数据的是 _TodoPageState

DART
final List<TodoItem> _todos = [];

7.3 setState 的作用

在 Flutter 中,直接修改变量并不会自动刷新页面。需要把修改放进 setState

DART
setState(() {
  _todos.add(TodoItem(title: title));
});

调用 setState 后,Flutter 会标记当前 State 需要更新,并在下一次构建阶段重新执行对应的 build 方法,然后根据新的 _todos 更新受影响的界面。

可以简单理解成:

Text
修改数据 → 调用 setState → Flutter 重新 build → 页面显示新数据

小项目里用 setState 足够了。等页面变多、数据需要多个页面共享时,再考虑 Provider、Riverpod、Bloc 等方案。

7.4 build 不是只执行一次

很多新手第一次看到 build 会以为它只负责初始化页面。实际上,只要相关状态变化,build 可能再次执行。

所以 build 里适合写“根据当前数据生成界面”的代码,不适合在里面直接发请求、创建不会复用的控制器,或者执行只需要做一次的操作。

7.5 为什么要释放 TextEditingController

TextEditingController 是一个需要管理生命周期的对象。页面销毁时要调用:

DART
@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

这不是为了让当前例子立刻不报错,而是养成习惯。以后页面里有动画控制器、滚动控制器、监听器时,同样要在 dispose 中清理。

8. 修改页面样式

Flutter 的样式主要写在 Widget 参数里。例如修改页面背景色:

DART
return Scaffold(
  backgroundColor: Colors.grey.shade100,
  appBar: AppBar(
    title: const Text('我的待办'),
  ),
  // 其他内容
);

修改主题颜色:

DART
theme: ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.deepPurple,
  ),
),

常用的布局 Widget:

Widget 用途
Row 水平排列
Column 垂直排列
Expanded 占满剩余空间
Padding 增加内边距
SizedBox 设置间距或固定宽高
Container 设置背景、边框、圆角等
ListView 显示可滚动列表
Stack 让 Widget 叠放

写布局时最容易遇到的问题是“溢出”。例如 Row 里放太长的文字,就可能出现右侧溢出警告。通常可以用 Expanded 包住需要自适应的内容,或者换成 ListView 让内容可以滚动。

9. Hot Reload、Hot Restart 和重新运行

开发时经常会用到下面几个操作:

操作 作用
Hot Reload 保留当前状态,快速加载代码修改
Hot Restart 重新启动 Dart 代码,状态会清空
Stop 后重新 Run 完整重新运行应用

flutter run 的终端里:

  • r:Hot Reload。
  • R:Hot Restart。
  • q:退出运行。

如果只是改了文字、颜色、布局,先按 r。如果改了初始化逻辑,但页面表现没有变化,可以按 R。修改 Android 原生配置、权限或 Gradle 文件后,通常需要停止应用再重新运行。

10. 添加第三方依赖

Flutter 使用 pubspec.yaml 管理依赖。比如添加网络请求库,可以直接在项目根目录执行:

PowerShell
flutter pub add http

这个命令会把当前兼容的 http 版本写入 pubspec.yaml,然后自动获取依赖。也可以手动编辑 pubspec.yaml,但新手阶段用命令更不容易写错缩进。

如果需要手动配置,就把 http 和当前兼容的版本号写到 dependencies 下,然后执行:

PowerShell
flutter pub get

代码中导入:

DART
import 'package:http/http.dart' as http;

实际项目里不要直接照抄一个很旧的版本号。可以去 pub.dev 查看当前版本,再根据项目的 Dart 和 Flutter 版本选择兼容版本。

添加依赖后如果编译失败,先看报错中有没有版本冲突,再检查 pubspec.yaml 的缩进。YAML 对缩进很敏感,建议只使用空格,不要混用 Tab。

11. 加载图片资源

在项目根目录创建目录:

Text
assets/images/

把图片放进去,例如:

Text
assets/images/logo.png

然后在 pubspec.yaml 中配置:

YAML
flutter:
  uses-material-design: true
  assets:
    - assets/images/logo.png

执行:

PowerShell
flutter pub get

代码中使用:

DART
Image.asset('assets/images/logo.png')

路径和文件名必须完全一致。改完 pubspec.yaml 后,如果 Hot Reload 没有显示新图片,可以停止应用再重新运行。

12. 页面跳转的最简单写法

当应用不止一个页面时,可以用 Navigator 跳转。先写一个详情页:

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('详情')),
      body: const Center(
        child: Text('这是详情页'),
      ),
    );
  }
}

然后在按钮点击时跳转:

DART
Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) => const DetailPage(),
  ),
);

返回上一页:

DART
Navigator.of(context).pop();

页面较少时,这种写法已经够用。页面很多、需要深层链接、Web URL 同步或统一管理路由时,可以进一步学习 go_router。对于大多数新项目,不建议把命名路由作为复杂导航的首选方案。

13. 常用命令

PowerShell
# 检查开发环境
flutter doctor

# 查看 Flutter 版本
flutter --version

# 查看设备
flutter devices

# 创建项目
flutter create project_name

# 获取依赖
flutter pub get

# 在 pubspec.yaml 当前版本约束范围内升级依赖
flutter pub upgrade

# 运行项目
flutter run

# 运行静态检查
flutter analyze

# 运行测试
flutter test

# 清理构建缓存
flutter clean

# 构建 Android APK
flutter build apk

flutter clean 不要一遇到问题就执行。它会删除构建产物,下一次运行需要重新编译,速度会明显变慢。只有在依赖、Gradle 或构建缓存出现异常时再用。

14. 常见问题

14.1 flutter 不是内部或外部命令

检查 C:\src\flutter\bin 是否加入 Path,然后关闭并重新打开终端。还可以直接检查文件是否存在:

Text
C:\src\flutter\bin\flutter.bat

14.2 flutter doctor 提示 Android toolchain 有问题

按提示逐项处理。常见原因有:

  • Android SDK 没有安装。
  • Command-line Tools 没有安装。
  • 没有执行 flutter doctor --android-licenses
  • Android SDK 路径没有被 Flutter 正确识别,或环境配置不一致。

处理完成后再次执行:

PowerShell
flutter doctor

不要只看第一次的报错,很多问题在前一项修好后会自动消失。

14.3 No devices found

先启动 Android 模拟器,再执行:

PowerShell
flutter devices

如果使用真机,需要打开开发者选项和 USB 调试,并在手机上允许这台电脑调试。Windows 还可能需要安装对应手机品牌的 USB 驱动。

14.4 Gradle 下载很慢或失败

第一次运行 Android 项目时,Gradle 和 Android 依赖可能需要下载较多文件。先确认网络正常,并注意报错里具体失败的是哪个地址。

不要一看到 Gradle 报错就随意修改 android 目录里的版本。Flutter、Android Gradle Plugin、Gradle 和 JDK 之间存在对应关系,随意升级其中一个,可能会带来更多问题。先运行 flutter doctor -v,确认当前使用的 Flutter、Java 和 Android SDK,再根据错误信息处理。

14.5 改完代码但页面没有变化

先按 r 做 Hot Reload;如果依然没有变化,按 R 做 Hot Restart。还不行就停止运行后重新执行:

PowerShell
flutter run

如果修改的是图片、字体、权限或原生配置,直接 Hot Reload 可能不会生效。

14.6 页面出现黄色黑色条纹

这是 Flutter 的布局溢出提示,不是装饰。常见原因是 Row 中的内容太宽、Column 内容超出屏幕,或者固定宽高不适合当前设备。

优先检查:

  • Row 中是否需要使用 Expanded
  • Column 是否需要放进 SingleChildScrollView
  • 是否写死了过大的宽度或高度。
  • 长文本是否需要换行或限制行数。

15. 接下来怎么继续学

上面的待办应用还没有保存数据,应用关闭后列表就没了。接下来可以按这个顺序扩展:

  1. 使用本地存储保存待办事项。
  2. 把待办列表拆成单独的 Widget。
  3. 增加详情页和页面跳转。
  4. 使用 http 请求后端接口。
  5. 处理加载中、空数据和请求失败状态。
  6. 根据项目大小选择 Provider、Riverpod 或 Bloc 等状态管理方案。
  7. 添加单元测试和 Widget 测试。
  8. 执行 flutter build apk,生成 Android 安装包。

建议每次只加一个功能。先让功能跑通,再拆文件、优化结构和处理异常。Flutter 初学阶段最重要的不是一次性学完所有框架,而是熟悉这条循环:

Text
写一点代码 → 运行 → 看界面 → 根据报错修改 → 再运行

16. 参考资料

阅读进度 0%