这篇文章记录一下我最近学习和开发 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。
刚开始只需要记住三件事:
- Flutter SDK:提供
flutter命令和框架代码。 - Dart SDK:已经包含在 Flutter SDK 里,不需要单独安装。
- Android Studio:主要用来提供 Android SDK、模拟器和相关工具。平时写代码可以使用 VS Code。
Flutter 的界面由 Widget 组成。文字、按钮、输入框、间距、页面本身,基本都可以看成 Widget。写页面的过程,就是把这些 Widget 一层一层组合起来。
2. 安装开发环境
2.1 安装 Git
Flutter 在安装和获取依赖时会用到 Git,所以先安装 Git:
安装时基本一路下一步即可。安装完成后打开 PowerShell,执行:
git --version
能看到版本号就说明 Git 已经安装好了。
2.2 下载 Flutter SDK
打开 Flutter 官方安装页面,下载 Windows 的 stable 版本:
建议把 SDK 解压到一个路径简单的目录,例如:
C:\src\flutter
不建议放到 C:\Program Files,也不要放在带空格、中文或权限比较严格的目录里。后面安装依赖和编译 Android 项目时,路径问题会让排查变得很麻烦。
解压完成后,Flutter 命令所在的位置是:
C:\src\flutter\bin
2.3 配置环境变量
把下面这个目录加入 Windows 的用户 Path:
C:\src\flutter\bin
配置方法:
- 在 Windows 搜索框输入“环境变量”。
- 打开“编辑系统环境变量”。
- 点击“环境变量”。
- 在“用户变量”中找到
Path,点击编辑。 - 新增
C:\src\flutter\bin。 - 一路确定,然后重新打开 PowerShell 和 VS Code。
检查 Flutter 是否能使用:
flutter --version
如果提示找不到 flutter,通常是终端没有重启,或者 Path 填错了。先关闭当前终端,重新打开再试。
2.4 安装 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,进入 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 一般可以在这里找到:
Android Studio → More Actions → SDK Manager
如果已经打开了一个项目,可以从菜单进入:
Tools → SDK Manager
Flutter 官方的 Android 环境说明会根据当前稳定版本更新具体 API Level,跟着官方页面选择推荐的稳定 SDK 即可,不需要死记某个版本号。
2.6 接受 Android SDK 协议
打开 PowerShell,执行:
flutter doctor --android-licenses
每次出现协议时输入 y 接受。最后看到类似下面的提示,说明协议处理完成:
All SDK package licenses accepted.
2.7 检查环境
执行:
flutter doctor
正常情况下会看到几项绿色的勾。刚安装好时不一定全部通过,先重点看 Android toolchain、Android Studio 和 VS Code 这几项。
还可以执行:
flutter devices
这个命令会列出 Flutter 当前识别到的设备。如果暂时没有设备,不代表 Flutter 安装失败,只是还没有启动模拟器或连接手机。
3. 创建 Android 模拟器
第一次学习建议使用模拟器,省去连接真机和 USB 驱动的麻烦。
打开 Android Studio 的 Device Manager:
More Actions → Virtual Device Manager
如果已经打开项目,可以从这里进入:
Tools → Device Manager
创建模拟器的步骤:
- 点击
Create Virtual Device。 - 选择一个 Phone,例如 Pixel 系列。
- 选择一个已经下载好的系统镜像。
- 如果没有系统镜像,点击下载,等待安装完成。
- 使用默认配置完成创建。
- 点击模拟器右侧的启动按钮。
模拟器启动后,在 PowerShell 中执行:
flutter devices
能看到一个 Android 设备就可以继续了。
如果模拟器启动很慢,或者启动后黑屏,先检查 BIOS 中是否开启虚拟化。Windows 也可能需要开启对应的虚拟机平台功能。电脑配置较低时,可以把模拟器分辨率和内存调低一些。
4. 创建第一个 Flutter 项目
先进入一个用来存放项目的目录。例如:
cd D:\flutter-projects
如果目录还不存在,可以先创建:
mkdir D:\flutter-projects
cd D:\flutter-projects
创建项目:
flutter create todo_demo
进入项目目录:
cd todo_demo
查看当前项目能运行在哪些设备上:
flutter devices
启动项目:
flutter run
如果有多个设备,Flutter 会让你选择一个。也可以指定设备运行:
flutter run -d emulator-5554
这里的 emulator-5554 要替换成 flutter devices 显示的设备 ID。
第一次编译 Android 项目可能比较慢,因为 Gradle 需要下载依赖。正常情况下,终端会持续输出下载或构建日志;如果长时间没有新的日志,或者反复出现下载失败,不要一直等待,应根据具体报错检查网络、Gradle 和 Android 工具链。首次构建完成后,之后再次运行通常会快很多。
5. 先看懂项目目录
用 VS Code 打开 todo_demo,先不用急着改代码,简单看一下结构。下面只列出本文会重点接触的主要目录;根据启用的平台不同,项目中还可能包含 windows/、macos/、linux/ 等目录:
todo_demo/
├─ android/ Android 原生工程配置
├─ ios/ iOS 工程配置
├─ lib/ 主要的 Dart 代码
│ └─ main.dart 程序入口
├─ test/ 测试代码
├─ web/ Web 平台文件
├─ pubspec.yaml 项目名称、依赖、资源配置
└─ analysis_options.yaml Dart 静态检查配置
平时最常修改的是两个地方:
lib/:写页面和业务代码。pubspec.yaml:添加第三方依赖、图片、字体等资源。
android、ios 等目录不是不能改,而是刚开始不要随便修改。很多 Android 编译问题都是改了原生配置后才出现的。
6. 做一个待办事项应用
现在把 lib/main.dart 里面的默认示例全部删除,替换成下面的代码。
这个版本故意没有使用 Provider、Riverpod、Bloc 等状态管理框架。项目还很小,先把 Flutter 最核心的状态变化弄明白更重要。
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 插件设置配置保存时自动刷新。
现在可以依次测试:
- 在输入框里输入一件事情。
- 点击“添加”,确认列表里出现任务。
- 点击左侧复选框,确认任务变成删除线。
- 点击右侧垃圾桶,确认任务被删除。
- 不输入内容直接点击“添加”,确认不会添加空任务。
这几个动作虽然简单,但已经覆盖了一个 Flutter 页面最常见的开发流程:读取输入、修改数据、刷新界面、响应点击和渲染列表。
7. 这段代码到底做了什么
7.1 main() 是程序入口,runApp() 挂载根 Widget
void main() {
runApp(const TodoApp());
}
Dart 程序从 main() 函数开始执行,它才是程序入口。runApp() 会接收一个 Widget,并把它挂载为应用的根 Widget;这里的 TodoApp 就是整个应用的根 Widget。
7.2 StatelessWidget 和 StatefulWidget
TodoApp 使用 StatelessWidget,因为它自身不需要维护可变状态。StatelessWidget 并不代表只会构建一次,在父组件或依赖发生变化时,它仍然可能重新执行 build。
TodoPage 使用 StatefulWidget,因为待办列表会变化:添加任务、勾选任务和删除任务都会改变数据。
真正保存变化数据的是 _TodoPageState:
final List<TodoItem> _todos = [];
7.3 setState 的作用
在 Flutter 中,直接修改变量并不会自动刷新页面。需要把修改放进 setState:
setState(() {
_todos.add(TodoItem(title: title));
});
调用 setState 后,Flutter 会标记当前 State 需要更新,并在下一次构建阶段重新执行对应的 build 方法,然后根据新的 _todos 更新受影响的界面。
可以简单理解成:
修改数据 → 调用 setState → Flutter 重新 build → 页面显示新数据
小项目里用 setState 足够了。等页面变多、数据需要多个页面共享时,再考虑 Provider、Riverpod、Bloc 等方案。
7.4 build 不是只执行一次
很多新手第一次看到 build 会以为它只负责初始化页面。实际上,只要相关状态变化,build 可能再次执行。
所以 build 里适合写“根据当前数据生成界面”的代码,不适合在里面直接发请求、创建不会复用的控制器,或者执行只需要做一次的操作。
7.5 为什么要释放 TextEditingController
TextEditingController 是一个需要管理生命周期的对象。页面销毁时要调用:
@override
void dispose() {
_controller.dispose();
super.dispose();
}
这不是为了让当前例子立刻不报错,而是养成习惯。以后页面里有动画控制器、滚动控制器、监听器时,同样要在 dispose 中清理。
8. 修改页面样式
Flutter 的样式主要写在 Widget 参数里。例如修改页面背景色:
return Scaffold(
backgroundColor: Colors.grey.shade100,
appBar: AppBar(
title: const Text('我的待办'),
),
// 其他内容
);
修改主题颜色:
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 管理依赖。比如添加网络请求库,可以直接在项目根目录执行:
flutter pub add http
这个命令会把当前兼容的 http 版本写入 pubspec.yaml,然后自动获取依赖。也可以手动编辑 pubspec.yaml,但新手阶段用命令更不容易写错缩进。
如果需要手动配置,就把 http 和当前兼容的版本号写到 dependencies 下,然后执行:
flutter pub get
代码中导入:
import 'package:http/http.dart' as http;
实际项目里不要直接照抄一个很旧的版本号。可以去 pub.dev 查看当前版本,再根据项目的 Dart 和 Flutter 版本选择兼容版本。
添加依赖后如果编译失败,先看报错中有没有版本冲突,再检查 pubspec.yaml 的缩进。YAML 对缩进很敏感,建议只使用空格,不要混用 Tab。
11. 加载图片资源
在项目根目录创建目录:
assets/images/
把图片放进去,例如:
assets/images/logo.png
然后在 pubspec.yaml 中配置:
flutter:
uses-material-design: true
assets:
- assets/images/logo.png
执行:
flutter pub get
代码中使用:
Image.asset('assets/images/logo.png')
路径和文件名必须完全一致。改完 pubspec.yaml 后,如果 Hot Reload 没有显示新图片,可以停止应用再重新运行。
12. 页面跳转的最简单写法
当应用不止一个页面时,可以用 Navigator 跳转。先写一个详情页:
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('这是详情页'),
),
);
}
}
然后在按钮点击时跳转:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DetailPage(),
),
);
返回上一页:
Navigator.of(context).pop();
页面较少时,这种写法已经够用。页面很多、需要深层链接、Web URL 同步或统一管理路由时,可以进一步学习 go_router。对于大多数新项目,不建议把命名路由作为复杂导航的首选方案。
13. 常用命令
# 检查开发环境
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,然后关闭并重新打开终端。还可以直接检查文件是否存在:
C:\src\flutter\bin\flutter.bat
14.2 flutter doctor 提示 Android toolchain 有问题
按提示逐项处理。常见原因有:
- Android SDK 没有安装。
- Command-line Tools 没有安装。
- 没有执行
flutter doctor --android-licenses。 - Android SDK 路径没有被 Flutter 正确识别,或环境配置不一致。
处理完成后再次执行:
flutter doctor
不要只看第一次的报错,很多问题在前一项修好后会自动消失。
14.3 No devices found
先启动 Android 模拟器,再执行:
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。还不行就停止运行后重新执行:
flutter run
如果修改的是图片、字体、权限或原生配置,直接 Hot Reload 可能不会生效。
14.6 页面出现黄色黑色条纹
这是 Flutter 的布局溢出提示,不是装饰。常见原因是 Row 中的内容太宽、Column 内容超出屏幕,或者固定宽高不适合当前设备。
优先检查:
Row中是否需要使用Expanded。Column是否需要放进SingleChildScrollView。- 是否写死了过大的宽度或高度。
- 长文本是否需要换行或限制行数。
15. 接下来怎么继续学
上面的待办应用还没有保存数据,应用关闭后列表就没了。接下来可以按这个顺序扩展:
- 使用本地存储保存待办事项。
- 把待办列表拆成单独的 Widget。
- 增加详情页和页面跳转。
- 使用
http请求后端接口。 - 处理加载中、空数据和请求失败状态。
- 根据项目大小选择 Provider、Riverpod 或 Bloc 等状态管理方案。
- 添加单元测试和 Widget 测试。
- 执行
flutter build apk,生成 Android 安装包。
建议每次只加一个功能。先让功能跑通,再拆文件、优化结构和处理异常。Flutter 初学阶段最重要的不是一次性学完所有框架,而是熟悉这条循环:
写一点代码 → 运行 → 看界面 → 根据报错修改 → 再运行
private note
交流
文章暂不开放公开评论。如果你有想法、问题或建议,欢迎私下联系站长。
联系站长 →