Flutter 教程 ------ 写给前端开发者
前置知识:已阅读 dart_tutorial.md。
核心心法:Flutter ≈ React 的思想 + 原生的渲染。你会 React/Vue,Flutter 的心智模型一天就能建立。
目录
- [Flutter 是什么?和 Web 前端的关系](#Flutter 是什么?和 Web 前端的关系)
- [最核心的心智模型:一切都是 Widget](#最核心的心智模型:一切都是 Widget)
- [解读本项目的 main.dart(逐行)](#解读本项目的 main.dart(逐行))
- [StatelessWidget vs StatefulWidget](#StatelessWidget vs StatefulWidget)
- [setState ≈ useState](#setState ≈ useState)
- [StatefulWidget 生命周期(对比 React)](#StatefulWidget 生命周期(对比 React))
- [布局系统:Row / Column / Stack ≈ Flexbox](#布局系统:Row / Column / Stack ≈ Flexbox)
- [常用 Widget 速查](#常用 Widget 速查)
- [样式思维转变:没有 CSS,一切都是 Widget](#样式思维转变:没有 CSS,一切都是 Widget)
- [BuildContext 与 InheritedWidget ≈ Context API](#BuildContext 与 InheritedWidget ≈ Context API)
- [页面导航 ≈ Router](#页面导航 ≈ Router)
- 状态管理
- [Key:列表 key 的知识可以直接迁移](#Key:列表 key 的知识可以直接迁移)
- 工程结构与常用命令
- [热重载 Hot Reload](#热重载 Hot Reload)
- 前端概念对照速查表
- 动手练习
1. Flutter 是什么?和 Web 前端的关系
| Web 前端 | Flutter | |
|---|---|---|
| UI 描述方式 | JSX / 模板(声明式) | Widget 组合(声明式,思想同 React) |
| 渲染目标 | 浏览器 DOM | 自绘引擎(Skia/Impeller),不依赖原生控件 |
| 布局语言 | CSS Flexbox | Row/Column/Flex(概念同 Flexbox,写法是 Widget 参数) |
| 样式 | CSS | Widget 的构造参数(padding、decoration...) |
| 事件处理 | onClick 属性 |
onPressed / GestureDetector 回调 |
| 状态管理 | useState / Redux / Pinia | setState / Provider / Riverpod / BLoC |
| 跨平台 | 浏览器 | iOS / Android / Web / 桌面 一套代码 |
关键认知:Flutter 不用原生控件,也不转 Webview,而是像游戏引擎一样自己画每一帧。 所以它不需要 CSS/DOM,性能和一致性都更高,代价是"样式"全用代码写。
2. 最核心的心智模型:一切都是 Widget
- Widget = UI 的不可变配置(类似 React 元素 / Vue 的 vnode 描述)。
- UI =
Widget树嵌套组合,build()方法就是你的 render 函数。 - 三棵树(了解即可,面试常问):
- Widget 树:不可变的配置描述(每次 rebuild 都重新创建,很轻量)
- Element 树:Widget 的实例化,管理生命周期和状态(≈ React Fiber)
- RenderObject 树:真正负责布局和绘制
dart
// Widget 就是层层嵌套的"配置对象",读法:由内向外 or 由外向内都行
Center( // 水平垂直居中容器
child: Text('Hello'), // 文本
)
和 JSX 对比:JSX 用
<div><span></span></div>,Flutter 用Container(child: Text(...))。子组件统一叫child(单个)或children(多个),写多了形成肌肉记忆。
3. 解读本项目的 main.dart(逐行)
下面是本项目的 lib/main.dart 结构(已省略注释),每一行都值得认识:
dart
import 'package:flutter/material.dart'; // ① Material Design 组件库(还有 cupertino = iOS 风格)
void main() {
runApp(const MyApp()); // ② runApp:把根 Widget 挂载到屏幕,≈ ReactDOM.render
}
class MyApp extends StatelessWidget { // ③ 无状态根组件
const MyApp({super.key}); // ④ 构造函数语法糖,key 是 Widget 身份标识
@override
Widget build(BuildContext context) { // ⑤ build ≈ render 函数
return MaterialApp( // ⑥ 应用级配置:路由/主题/国际化
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: .fromSeed(seedColor: Colors.deepPurple), // ⑦
),
home: const MyHomePage(title: 'Flutter Demo Home Page'), // ⑧ 首页
);
}
}
class MyHomePage extends StatefulWidget { // ⑨ 有状态组件的"配置类"
const MyHomePage({super.key, required this.title});
final String title; // ⑩ Widget 字段全部 final(不可变配置)
@override
State<MyHomePage> createState() => _MyHomePageState(); // ⑪ 创建状态对象
}
class _MyHomePageState extends State<MyHomePage> { // ⑫ 状态类(私有,_ 开头)
int _counter = 0; // ⑬ 这里才放可变状态
void _incrementCounter() {
setState(() { // ⑭ ≈ useState 的 setter
_counter++;
});
}
@override
Widget build(BuildContext context) { // ⑮ setState 后重新执行这里
return Scaffold( // ⑯ 页面脚手架:appBar+body+FAB 布局
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title), // ⑰ widget.xxx 访问外部传入的配置
),
body: Center(
child: Column(
mainAxisAlignment: .center, // ⑱
children: [
const Text('You have pushed the button this many times:'),
Text('$_counter', style: Theme.of(context).textTheme.headlineMedium),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter, // ⑲ 点击回调
child: const Icon(Icons.add),
),
);
}
}
你注意到的两个奇怪写法
⑦ .fromSeed(...) 和 ⑱ .center ------ 这是 Dart 3.10+ 的点简写(dot shorthand):编译器能从上下文推断类型时,可以省略类型前缀:
dart
colorScheme: .fromSeed(seedColor: Colors.deepPurple)
// 等价于
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)
mainAxisAlignment: .center
// 等价于
mainAxisAlignment: MainAxisAlignment.center
JS 里没有对应物,看到 .xxx 开头就是这种简写。
其他关键点
- ⑨⑫ StatefulWidget 被拆成两个类:Widget 类(不可变配置)+ State 类(可变状态)。这是 Flutter 最特殊的模式,先记住,第 4 节解释为什么。
- ⑰
widget.title:在 State 类里通过widget属性访问外部传入的配置(因为 title 不在 State 里)。 - ⑯ Scaffold:Material 页面脚手架,提供 appBar / body / 底部导航 / FAB 等标准布局坑位。
4. StatelessWidget vs StatefulWidget
| StatelessWidget | StatefulWidget | |
|---|---|---|
| 有无可变状态 | 无 | 有(存在 State 对象里) |
| React 对应 | 纯函数组件 | class 组件 / useState 组件 |
| 何时用 | 纯展示、props 进 UI 出 | 计数器、表单、动画、网络数据 |
为什么拆成两个类? Widget 是不可变的(immutable),每次重建都新建 Widget 实例;而 State 是持久的,挂在 Element 树上不随 rebuild 销毁。这样 diff 时用"新 Widget 配置 + 旧 State"重建 UI,状态得以保留。
dart
// 标准三件套模板(背下来,天天写)
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count = 0;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => setState(() => count++),
child: Text('$count'),
);
}
}
5. setState ≈ useState
dart
// React:
// const [count, setCount] = useState(0);
// setCount(count + 1);
// Flutter:
int count = 0;
void increment() {
setState(() { // 传入一个回调,在回调里改状态
count++;
});
}
三条铁律:
- 改状态必须包在
setState里 ,直接改count++不会触发重绘(不报错但 UI 不更新,新手第一大坑)。 setState的回调要保持轻量------只做状态修改;重活放在外面做。- 频繁重建时用
constWidget(如const Text(...))标记不变的部分,Flutter 会跳过它们。
6. StatefulWidget 生命周期(对比 React)
dart
class _MyState extends State<MyState> {
// ≈ useEffect(() => {...}, []) ------ 挂载时执行一次
@override
void initState() {
super.initState();
// 初始化状态、订阅事件、发起网络请求
}
// ≈ useEffect(() => { return cleanup }, [deps])
@override
void didUpdateWidget(MyState old) {
super.didUpdateWidget(old);
// 父组件传入了新配置
}
@override
Widget build(BuildContext context) { ... }
// ≈ useEffect 的 cleanup ------ 卸载时执行
@override
void dispose() {
// 取消订阅、销毁 controller(**必须**,否则内存泄漏)
super.dispose();
}
}
| React | Flutter |
|---|---|
useEffect(fn, []) |
initState() |
useEffect cleanup |
dispose() |
useEffect(fn, [dep]) |
didUpdateWidget() / didChangeDependencies() |
useRef |
TextEditingController 等各类 controller |
| 组件销毁 | dispose() |
7. 布局系统:Row / Column / Stack ≈ Flexbox
没有 CSS,布局就是嵌套 Widget:
dart
// Column = flex-direction: column
Column(
mainAxisAlignment: MainAxisAlignment.spaceAround, // 主轴 ≈ justify-content
crossAxisAlignment: CrossAxisAlignment.center, // 交叉轴 ≈ align-items
children: [ Text('A'), Text('B') ],
)
// Row = flex-direction: row(主轴变成水平)
Row(
children: [
Text('左'),
Spacer(), // ≈ flex-grow 占位弹簧
Text('右'),
],
)
// Stack = position: relative + absolute
Stack(
children: [
Image.network(url),
Positioned(right: 8, top: 8, child: Badge()), // ≈ absolute 定位
],
)
// 常用"原子"布局 Widget
Container(...) // div:颜色/边距/圆角/装饰全能干(但成本高,能用专用的就别用)
Padding(...) // ≈ padding
Align(...) // ≈ text-align / margin auto
Expanded(flex: 1, child: ...) // ≈ flex: 1
Flexible(...) // ≈ flex-shrink
SizedBox(width: 10, height: 10) // 固定尺寸 / 占位间距(≈ margin 的常用替代)
主轴/交叉轴记忆法 :Column 主轴是竖直,Row 主轴是水平;mainAxis 沿着排列方向,crossAxis 垂直于它。
常见布局坑
- Row/Column 里放没有约束的内容会溢出(屏幕右侧黄黑条纹 = overflow),用
Expanded/Flexible包住。 ListView(可滚动列表)里嵌Column时注意滚动方向冲突。- 文本换行:
Text默认不换行,需要TextOverflow.ellipsis或外层Flexible。
8. 常用 Widget 速查
dart
// ── 文本与图片 ──
Text('hello', style: TextStyle(fontSize: 16, color: Colors.red, fontWeight: FontWeight.bold))
Image.network('https://...') // 网络图(≈ img src)
Image.asset('assets/logo.png') // 本地图(需在 pubspec.yaml 声明)
Icon(Icons.home, size: 24) // 图标(内置 Material 图标集)
// ── 容器 ──
Container(
padding: EdgeInsets.all(16), // ≈ padding(EdgeInsets 有 all/symmetric/only)
margin: EdgeInsets.only(top: 8),
decoration: BoxDecoration( // ≈ background + border-radius + box-shadow
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(blurRadius: 4)],
),
child: Text('content'),
)
// ── 按钮 ──
FilledButton(onPressed: () {}, child: Text('确定')) // ≈ 主按钮
TextButton(onPressed: () {}, child: Text('取消')) // ≈ 文字按钮
OutlinedButton(onPressed: () {}, child: Text('次要')) // ≈ 描边按钮
IconButton(onPressed: () {}, icon: Icon(Icons.add))
// ── 输入(需要 controller,≈ useRef + value/onChange 合体)──
final controller = TextEditingController();
TextField(
controller: controller, // controller.text 读值
onChanged: (v) => print(v), // ≈ onChange
decoration: InputDecoration(labelText: '用户名'),
)
// ── 列表 ──
ListView( // 简单列表(一次性构建)
children: [ ListTile(title: Text('item 1')) ],
)
ListView.builder( // 懒加载列表(≈ 虚拟滚动,大数据必用)
itemCount: 1000,
itemBuilder: (context, index) => ListTile(title: Text('item $index')),
)
// ── 条件渲染:没有 JSX 的 {cond && <A/>},用 Dart 表达式 ──
Column(children: [
if (isLoading) CircularProgressIndicator(),
if (error != null) Text(error!) else Text('ok'),
...items.map((i) => ListTile(title: Text(i))), // 列表渲染:展开 map
])
注意列表渲染没有 key 参数,
...items.map(...)生成的列表直接放进 children。对话框/浮层这类命令式 UI 不写在 build 里:
dartshowDialog(context: context, builder: (_) => AlertDialog(title: Text('提示'))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存成功'))); // ≈ toast
9. 样式思维转变:没有 CSS,一切都是 Widget
| CSS | Flutter |
|---|---|
padding: 16px |
Padding(padding: EdgeInsets.all(16)) 或 Container(padding:...) |
border-radius: 8px |
BoxDecoration(borderRadius: ...) 或 ClipRRect |
color: red |
Container(color: ...) / BoxDecoration(color:...) |
display: flex |
Row / Column |
position: absolute |
Stack + Positioned |
overflow-y: auto |
SingleChildScrollView / ListView |
border: 1px solid |
BoxDecoration(border: Border.all()) |
| rem/em | 逻辑像素(double,已自动处理多倍屏) |
| 媒体查询 | MediaQuery.of(context).size |
| 深色模式 | Theme.of(context).brightness == Brightness.dark |
单位:Flutter 用逻辑像素(类似 rem 的概念),不用操心 DPR。
10. BuildContext 与 InheritedWidget ≈ Context API
BuildContext = Widget 在树中的"位置引用"(≈ React fiber 节点引用)。用它向上查找祖先提供的数据:
dart
Theme.of(context) // 读全局主题(框架内置的 InheritedWidget)
MediaQuery.of(context) // 读屏幕尺寸等信息
// 自己写跨层传数据:InheritedWidget ≈ React Context.Provider
// 实际开发很少手写,直接用 Provider / Riverpod 包(见下一节)
规则同 React:context 查找只向上(祖先方向) ;build 外使用 context 前先判断 mounted(≈ React 的"组件可能已卸载"):
dart
Future<void> fetchData() async {
final data = await api.load();
if (!mounted) return; // async 之后 context 可能已失效,先检查
setState(() => items = data);
}
11. 页面导航 ≈ Router
dart
// 命名路由(简单场景)
MaterialApp(
initialRoute: '/',
routes: {'/detail': (ctx) => DetailPage()},
);
Navigator.of(context).pushNamed('/detail'); // ≈ navigate('/detail')
Navigator.of(context).pop(); // ≈ navigate(-1)
// 直接 push Widget(原型阶段常用)
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => DetailPage(id: 42)),
);
// 带参数返回(≈ resolve 后拿到结果)
final result = await Navigator.of(context).push(...);
复杂 App 用 go_router(官方推荐的声明式路由,≈ React Router:路径、嵌套路由、守卫、深链)。
12. 状态管理
演进路线(≈ React 生态的演进):
| 层级 | Flutter | React 对应 |
|---|---|---|
| 组件内 | setState |
useState |
| 父子传递 | 构造参数 + 回调 | props + callback |
| 跨层共享 | InheritedWidget / Provider | Context API |
| 复杂应用 | Riverpod / BLoC | Redux / Zustand |
| 服务端状态 | --- | React Query |
入门先掌握 setState + Provider;Riverpod 是当前社区主流(编译安全、不用 context、支持异步),需要时再学。
13. Key:列表 key 的知识可以直接迁移
和 React 一样,列表项有身份语义时要给 key,帮助框架 diff:
dart
ListView(children: [
for (final item in items)
ListTile(key: ValueKey(item.id), title: Text(item.name)),
])
常用:ValueKey(值身份)、ObjectKey、UniqueKey(≈ 强制重建)。默认隐式 key 的 diff 规则和 React 类似------同类型、同位置会复用 State,这也是"同类列表增删时状态串位"问题的同源原因。
14. 工程结构与常用命令
flutter_application_1/
├── lib/ # ← 你的代码全在这(≈ src/)
│ └── main.dart
├── android/ # Android 壳工程(一般不改)
├── ios/ # iOS 壳工程(一般不改)
├── test/ # 单元/组件测试(≈ *.test.ts)
├── pubspec.yaml # ≈ package.json(依赖+资源声明)
└── analysis_options.yaml # ≈ eslint 配置
| 命令 | 作用 |
|---|---|
flutter run |
运行到模拟器/真机(≈ npm run dev) |
flutter run -d chrome |
跑到浏览器 |
flutter devices |
列出可用设备 |
flutter create <name> |
新建项目 |
flutter pub get |
安装依赖(≈ npm i) |
flutter analyze |
静态检查(≈ lint + tsc) |
flutter test |
跑测试 |
flutter build apk / build ios |
打包发布 |
15. 热重载 Hot Reload
flutter run 运行后,终端快捷键:
| 按键 | 作用 | ≈ Web |
|---|---|---|
r |
热重载 :毫秒级刷新 UI,保留页面状态 | HMR |
R |
热重启:重置状态、重新执行 main | 刷新页面 |
p |
显示渲染边界线框(调试布局神器) | DevTools 的 layers |
q |
退出 | --- |
改代码保存 → 自动热重载(IDE 支持)。注意:改了 initState/全局变量/main() 等初始化逻辑时,热重载不会生效,需要 R 热重启。
16. 前端概念对照速查表
| Web 前端 | Flutter |
|---|---|
| 组件 | Widget |
| render / 模板 | build(BuildContext context) |
| props | final 字段 + 构造参数(命名参数) |
| state | State 类字段 + setState |
useState |
setState |
useEffect(fn, []) |
initState() |
| cleanup | dispose() |
| Context API | InheritedWidget / Provider |
| children | child / children |
| div | Container |
| flex | Row / Column + Expanded |
| position: absolute | Stack + Positioned |
<img> |
Image.network / Image.asset |
<input> |
TextField + TextEditingController |
| 虚拟滚动 | ListView.builder |
| onClick | onPressed / GestureDetector |
| React Router | Navigator / go_router |
| toast | SnackBar |
| modal | showDialog |
| CSS | 无,Widget 参数 + BoxDecoration |
| media query | MediaQuery.of(context) |
| key | Key / ValueKey |
| HMR | Hot Reload(r) |
17. 动手练习
练习目标 :改本项目的 lib/main.dart,做一个"待办清单"页面,覆盖:StatefulWidget、setState、列表渲染、输入框、条件渲染、导航。
dart
// 把 MyHomePageState 换成下面内容即可体验(AppBar/body 相应调整)
class _MyHomePageState extends State<MyHomePage> {
final _controller = TextEditingController();
final _todos = <String>[];
@override
void dispose() {
_controller.dispose(); // 别忘了销毁 controller
super.dispose();
}
void _addTodo() {
final text = _controller.text.trim();
if (text.isEmpty) return;
setState(() {
_todos.add(text);
_controller.clear();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(child: TextField(controller: _controller)),
IconButton(onPressed: _addTodo, icon: const Icon(Icons.add)),
],
),
),
Expanded(
child: _todos.isEmpty
? const Center(child: Text('暂无待办'))
: ListView.builder(
itemCount: _todos.length,
itemBuilder: (context, i) => ListTile(
leading: const Icon(Icons.check_circle_outline),
title: Text(_todos[i]),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => setState(() => _todos.removeAt(i)),
),
),
),
),
],
),
);
}
}
进阶学习路线
- 布局实战:仿写一个设置页(ListTile + Switch + 分组卡片)
- 网络请求 :
http包 +FutureBuilder(≈ Suspense 思路) - 状态管理:Provider → Riverpod
- 路由:go_router
- 官方教程:https://docs.flutter.dev/get-started/codelab (第一个 Flutter 应用 codelab,强烈推荐)
本教程基于 Flutter(Material 3)+ Dart 3.x,示例均适配本项目 SDK 环境下的 main.dart。