Flutter / Dart 学习笔记
目录
Dart 语言基础
- const 关键字
- Dart 命名规范
- 访问控制:公开与私有
- 类的标准结构
- final / const / var / late
- Null Safety 操作符
- 常用集合操作
- async / await 异步
- Dart 3.0 新特性
- 集合字面量控制流
Flutter 开发实践
- 常用函数类型
- BuildContext 是什么
- Key 的使用场景
- TextField 与 Controller
- dispose 机制
- 集合字面量控制流
- TextField 与 Controller
- dispose 机制
1. const 关键字
什么时候可以加 const?
const 加在编译期就能确定值的地方,需要同时满足:
- 构造函数本身声明了
const - 所有参数都是编译期常量(字面量、
const变量、其他const对象)
// ✅ 可以加 const:参数都是字面量
const Text('hello', style: TextStyle(fontSize: 18))
// ❌ 不能加 const:参数是运行时变量
Text(userName)
Text('count: $count')
// ❌ 不能加 const:方法调用不是构造函数
TextDecoration.combine([TextDecoration.underline, TextDecoration.lineThrough])
口诀:内容固定不变 → 加 const;内容依赖变量/状态 → 不能加
为什么要加 const?
Flutter 会复用 const widget 对象,setState 重建时直接跳过,不重新创建。对静态内容(标题、固定文字、间距)有性能收益。
父组件重建时,子组件是否跟着重建?
决定是否跳过的是 const,不是参数数量:
// 有变量参数 → 不能 const → 父重建时子跟着重建
_StatelessChild(parentBuildCount: _rebuildCount)
// 无参数,不加 const → 父重建时子跟着重建
_StatelessChild()
// 无参数,加 const → 父重建时子跳过 ✅
const _StatelessChild()
// const 内置 widget 同理 → 跳过 ✅
const Text('标题')
const SizedBox(height: 16)
const 是跳过重建的开关,与参数数量无关。
2. Dart 命名规范
命名风格
| 场景 | 风格 | 例子 |
|---|---|---|
| 变量、方法、参数 | camelCase | userName, onTap |
| 类、枚举、类型 | PascalCase | MyWidget, ColorScheme |
| 常量 | camelCase(不用全大写) | const defaultPadding = 8.0 |
| 文件名 | snake_case | d04.dart, home_page.dart |
| 私有任何东西 | 加 _ 前缀 | _count, _build() |
常见惯例
// State 类用下划线私有,外部无需访问
class _Demo04State extends State<Demo04_Stateful> {}
// 回调参数习惯用 on 开头
final VoidCallback onTap;
final ValueChanged<String> onChanged;
// bool 变量习惯 is/has/can 开头
bool isLoading = false;
bool hasError = false;
Dart 不像 Java 用
private关键字,下划线是唯一的私有机制,作用域是整个文件(不是类)。
3. 访问控制:公开与私有
核心原则
只在本文件用就加 _,需要外部访问才公开。
super.key 什么时候写?
// 公开 widget → 写 super.key(外部调用方可能需要传 key)
class MyButton extends StatelessWidget {
const MyButton({super.key});
}
// 私有 widget → 省略(外部访问不到,传 key 没意义)
class _StatelessChild extends StatelessWidget {
const _StatelessChild({required this.parentBuildCount});
}
key 的作用: 让 Flutter 在列表增删排序、跨帧重建时精准识别 widget 身份。
组件封装原则
对外只暴露入口组件,内部子组件全部私有:
HomePage ← 公开(路由/其他文件要用)
_HeaderSection ← 私有
_Avatar ← 私有
_Title ← 私有
_ProductCard ← 私有
需要跨文件复用时,才单独建文件并公开:
widgets/
product_card.dart → class ProductCard(公开,多处复用)
pages/
home.dart → class HomePage(公开)+ class _Header(私有)
各类型的私有规则
| 场景 | 只本文件用 | 跨文件复用 |
|---|---|---|
| 类 / Widget | _MyClass | MyClass |
| 属性 / 变量 | _count | count |
| 顶层常量 | _kSpacing | kSpacing |
| 枚举 | _TabIndex | TabIndex |
| 抽象类 | _BaseItem | BaseItem |
// 顶层常量
const double defaultPadding = 16.0; // 公开
const double _innerSpacing = 8.0; // 私有
// 枚举
enum LoadingState { idle, loading, success, error } // 公开
enum _TabIndex { home, profile, settings } // 私有
// 抽象类
abstract class Repository { ... } // 公开
abstract class _BaseItem { ... } // 私有
一句话:能私有就私有,需要复用再公开。
4. 类的标准结构
声明 → 构造 → 使用(顺序固定)
class _ProductCard extends StatelessWidget {
// 1. 声明属性
final String name;
final double price;
// 2. 构造函数接收参数
const _ProductCard({
required this.name,
required this.price,
});
// 3. 方法里使用属性
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(name),
Text('¥$price'),
],
);
}
}
// 调用
_ProductCard(name: '苹果', price: 9.9)
// 可选参数(需要默认值)
const _ProductCard({
required this.name,
this.price = 0.0, // 不传则默认 0.0
});
语法糖 vs 完整写法
// Dart 简写(语法糖)— 实际开发都用这种
const _ProductCard({
required this.name,
required this.price,
});
// 等价的完整写法
const _ProductCard({
required String name,
required double price,
}) : this.name = name,
this.price = price;
: this.name = name 叫初始化列表,在构造函数体执行前把参数赋值给属性。this.xxx 语法糖把这两步合并了。
5. final / const / var / late
四者区别
// var:类型推断,可重新赋值
var name = 'Alice';
name = 'Bob'; // ✅
// final:运行时确定,赋值后不可变
final name = 'Alice';
final now = DateTime.now(); // ✅ 运行时才确定
name = 'Bob'; // ❌ 编译报错
// const:编译期确定,赋值后不可变
const pi = 3.14;
const now = DateTime.now(); // ❌ 运行时才能得到,不能 const
// late:延迟初始化,声明时不赋值,使用前再赋值
late String name;
name = fetchName(); // 使用前赋值即可
日常选择原则
| 场景 | 用哪个 |
|---|---|
| 编译期常量(字面量、固定值) | const |
| 运行时确定、之后不变 | final |
| 需要重新赋值 | var 或直接写类型 |
| 初始化依赖其他逻辑,但一定会在使用前赋值 | late |
late 典型用法
// 1. 依赖 initState 初始化的 Controller
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this); // 这里才能初始化
}
// 2. 测试中的延迟初始化
late MyService service;
setUp(() => service = MyService());
late使用前未赋值会抛运行时异常,确保赋值路径覆盖所有情况。
6. Null Safety 操作符
五个核心操作符
String? name; // ? 声明可为 null
// ?. 安全调用:为 null 时返回 null,不崩溃
name?.length // null
name?.toUpperCase() // null
// ?? 空合并:为 null 时取右边的值
name ?? 'Guest' // → 'Guest'
name?.length ?? 0 // → 0
// ??= 空合并赋值:为 null 时才赋值
name ??= 'Guest' // name 为 null 则赋值,否则保持原值
// ! 非空断言:告诉编译器"我确定不为 null"
name!.length // 若实际为 null 则运行时崩溃,慎用
实际场景
// 从 Map 取值(可能为 null)
final user = map['user'];
final displayName = user?.name ?? '匿名用户';
// 链式调用
order?.address?.city ?? '未知城市'
// 列表过滤 null
final items = rawList.whereType<String>().toList();
与 React 对比
// JS 可选链
user?.name ?? 'Guest'
// Dart 完全一样
user?.name ?? 'Guest'
7. 常用集合操作
React 开发者最熟悉的 map / filter / reduce,在 Dart 里对应:
map → map
final names = ['Alice', 'Bob'];
// Dart
final upper = names.map((n) => n.toUpperCase()).toList();
// 注意:Dart 的 map 返回 Iterable,需要 .toList() 转换
filter → where
final numbers = [1, 2, 3, 4, 5];
final evens = numbers.where((n) => n % 2 == 0).toList(); // [2, 4]
find → firstWhere
final users = [User(id: 1), User(id: 2)];
// 找到返回对象,找不到抛异常
final user = users.firstWhere((u) => u.id == 1);
// 找不到返回 null(安全版本)
final user = users.firstWhereOrNull((u) => u.id == 99); // null
reduce → fold
final numbers = [1, 2, 3, 4];
// fold(带初始值,更安全)
final sum = numbers.fold(0, (acc, n) => acc + n); // 10
// reduce(无初始值,空列表会抛异常)
final sum = numbers.reduce((acc, n) => acc + n); // 10
其他常用
final list = [1, 2, 3, 4, 5];
list.any((n) => n > 3) // true — 是否有满足条件的
list.every((n) => n > 0) // true — 是否全部满足
list.contains(3) // true
list.length // 5
list.isEmpty // false
list.isNotEmpty // true
list.first // 1
list.last // 5
list.reversed.toList() // [5, 4, 3, 2, 1]
list.take(3).toList() // [1, 2, 3]
list.skip(2).toList() // [3, 4, 5]
list.sort((a, b) => a - b) // 升序排序(原地修改)
// 展开嵌套列表
[[1, 2], [3, 4]].expand((e) => e).toList() // [1, 2, 3, 4]
Map 常用
final map = {'name': 'Alice', 'age': 30};
map.keys.toList() // ['name', 'age']
map.values.toList() // ['Alice', 30]
map.entries // Iterable<MapEntry>
map.containsKey('name') // true
map['name'] // 'Alice'(可能为 null)
map['name'] ?? '' // 安全取值
8. async / await 异步
基础写法
// Future:单次异步结果(对应 Promise)
Future<String> fetchUser() async {
final response = await http.get(url);
return response.body;
}
// 调用
final user = await fetchUser();
// try-catch 处理错误
try {
final user = await fetchUser();
} catch (e) {
debugPrint('错误: $e');
}
与 React useEffect 对比
// React
useEffect(() => {
fetchUser().then(setUser)
}, [])
// Flutter — 在 initState 里发起异步
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
final data = await fetchUser();
if (mounted) setState(() => _user = data); // mounted 检查,防止组件已销毁
}
FutureBuilder(异步 UI 三态)
FutureBuilder<String>(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator(); // 加载中
}
if (snapshot.hasError) {
return Text('错误: ${snapshot.error}'); // 出错
}
return Text(snapshot.data!); // 成功
},
)
Stream(持续的数据流)
// Stream:多次异步结果(对应 RxJS Observable)
Stream<int> countDown() async* {
for (int i = 3; i >= 0; i--) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
// StreamBuilder
StreamBuilder<int>(
stream: countDown(),
builder: (context, snapshot) => Text('${snapshot.data}'),
)
9. Dart 3.0 新特性
Records(记录)
轻量组合多个值,无需定义类。
// 匿名字段(位置),用 $1 $2 访问
var r = (42, 'hello', true);
r.$1 // → 42
r.$2 // → 'hello'
// 命名字段,用字段名访问
var r = (id: 42, name: 'hello');
r.id // → 42
r.name // → 'hello'
// 类型声明
(int, String) r = (42, 'hello');
({int id, String name}) r = (id: 42, name: 'hello');
// 函数返回多个值
(int, String) getUser() => (1, 'Alice');
var (id, name) = getUser(); // 解构赋值
// for 循环解构(比 $1 $2 更清晰)
for (final (alignment, label) in list) { ... }
特点:值类型(字段相同则相等)、不可修改、适合临时组合数据。
Patterns(模式匹配)
// switch 解构
switch (point) {
case (0, 0): print('原点');
case (var x, 0): print('x轴 $x');
case (var x, var y): print('($x, $y)');
}
// if-case
if (json case {'name': String name}) {
print(name);
}
// 列表 / Map 解构
var [a, b, ...rest] = [1, 2, 3, 4];
var {'name': name} = map;
Class Modifiers(类修饰符)
base class Animal {} // 只能在本库内继承
interface class Drawable {} // 只能被实现,不能被继承
final class Config {} // 既不能继承也不能实现
// sealed:子类必须穷举,配合 switch 编译器兜底
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
switch (shape) {
case Circle(): ...
case Square(): ... // 少写一个编译器报错
}
switch 表达式 + 守卫子句
// switch 直接作为表达式赋值
String label = switch (code) {
200 => 'OK',
404 => 'Not Found',
_ => 'Unknown',
};
// 守卫子句 when
switch (point) {
case (var x, var y) when x == y:
print('对角线上');
}
Sound Null Safety 完整落地
Dart 3.0 起彻底移除对非空安全代码的兼容,所有代码必须是 null-safe。
总结
| 特性 | 用途 |
|---|---|
| Records | 轻量组合多值,替代临时 Map/类 |
| Patterns | 解构 + 匹配,简化条件判断 |
| sealed class | 穷举子类,编译器兜底 |
| Class modifiers | 精确控制类的继承/实现权限 |
| switch 表达式 | switch 直接返回值 |
| Guard clause | switch 分支加条件 |
10. 集合字面量控制流
Dart 在 [...] / {...} 内部支持 for / if / ...,可直接在 children 里内联逻辑。
collection for
// 基础
children: [
for (final i in [1, 2, 3])
Text('第 $i 项'),
]
// 带索引(Dart 3.0+)
children: [
for (final (i, item) in list.indexed)
Text('$i: ${item.name}'),
]
// 嵌套循环
children: [
for (final row in matrix)
for (final cell in row)
Text('$cell'),
]
collection if
// 单条件
children: [
if (isVip) Icon(Icons.star),
]
// if-else
children: [
if (isLoading)
const CircularProgressIndicator()
else
const Text('加载完成'),
]
// 多条件
children: [
if (user != null) Text(user.name),
if (user != null && user.isAdmin) Text('管理员'),
if (errorMsg != null) Text(errorMsg!, style: TextStyle(color: Colors.red)),
]
展开运算符 ... / ...?
// 展开列表
AppBar(
actions: [
IconButton(icon: Icon(Icons.search), onPressed: () {}),
...actions, // 展开插入
],
)
// ...? 空安全展开,null 时直接跳过
children: [
Text('固定内容'),
...?extraWidgets,
]
// 展开多个
children: [
...headerWidgets,
...bodyWidgets,
...footerWidgets,
]
组合示例
children: [
const Text('商品列表'),
if (isLoading)
const CircularProgressIndicator()
else ...[
for (final product in products)
if (product.inStock)
ListTile(title: Text(product.name)),
if (products.isEmpty)
const Text('暂无商品'),
],
...?footerWidgets,
]
11. 常用函数类型
Flutter 内置了几个高频函数类型,直接用不需要自己写完整类型:
// VoidCallback:无参无返回值
final VoidCallback onTap;
onTap: () => print('点击')
// ValueChanged<T>:接收一个值,无返回值
final ValueChanged<String> onChanged;
onChanged: (value) => setState(() => _text = value)
// ValueGetter<T>:无参,返回一个值
final ValueGetter<int> getCount;
getCount: () => _count
// WidgetBuilder:接收 context,返回 Widget
final WidgetBuilder builder;
builder: (context) => Text('hello')
// IndexedWidgetBuilder:带索引的 WidgetBuilder
final IndexedWidgetBuilder itemBuilder;
itemBuilder: (context, index) => ListTile(title: Text('$index'))
与完整写法对比
// 完整写法
final void Function() onTap;
final void Function(String) onChanged;
final int Function() getCount;
final Widget Function(BuildContext) builder;
// Flutter 内置简写(推荐)
final VoidCallback onTap;
final ValueChanged<String> onChanged;
final ValueGetter<int> getCount;
final WidgetBuilder builder;
自定义函数类型
// typedef 定义复杂函数类型
typedef OnSubmit = void Function(String text, bool isValid);
class MyForm extends StatelessWidget {
final OnSubmit onSubmit;
const MyForm({required this.onSubmit});
}
12. BuildContext 是什么
本质
BuildContext 是 widget 在组件树中的位置句柄,通过它可以:
- 向上查找父级 widget(
Theme.of(context)、Navigator.of(context)) - 获取屏幕尺寸(
MediaQuery.of(context)) - 跳转路由、显示 SnackBar 等
// 常见用法
Theme.of(context).colorScheme.primary // 获取主题色
MediaQuery.of(context).size.width // 屏幕宽度
Navigator.of(context).push(...) // 路由跳转
ScaffoldMessenger.of(context).showSnackBar(...) // 显示提示
为什么不能跨帧随意使用
BuildContext 绑定的是某一帧的 widget 位置,widget 销毁后 context 就失效了。在异步操作后使用可能导致崩溃:
// ❌ 危险:await 之后 widget 可能已销毁
Future<void> _load() async {
final data = await fetchData();
Navigator.of(context).push(...); // context 可能已失效
}
// ✅ 正确:先检查 mounted
Future<void> _load() async {
final data = await fetchData();
if (!mounted) return; // widget 已销毁则直接返回
Navigator.of(context).push(...);
}
mounted 是什么
mounted 是 State 的属性,表示当前组件是否还在组件树中:
if (mounted) setState(() => _data = data); // 安全的 setState
13. Key 的使用场景
Key 是什么
Key 帮助 Flutter 在组件树重建时精准识别同类型 widget 的身份,默认情况下 Flutter 按位置匹配。
什么时候需要 Key
不需要 Key 的情况(大多数): 静态列表、固定结构的 UI。
需要 Key 的情况:
// 1. 动态列表有增删排序操作
ListView(
children: items.map((item) =>
ItemWidget(key: ValueKey(item.id), item: item) // ValueKey 用唯一 id
).toList(),
)
// 2. 同类型 widget 需要保持各自的 State
Row(
children: [
TextField(key: ValueKey('email')),
TextField(key: ValueKey('phone')),
],
)
三种常用 Key
// ValueKey:用一个值标识,最常用
ValueKey(item.id)
ValueKey('email')
// GlobalKey:可以在任何地方访问对应 widget 的 State
final _formKey = GlobalKey<FormState>();
Form(key: _formKey, ...)
_formKey.currentState?.validate() // 跨组件调用方法
// UniqueKey:每次 build 都生成新 key,强制重建 widget
UniqueKey() // 用于强制刷新某个子树
与 React 对比
// React 列表的 key
items.map(item => <Item key={item.id} />)
// Flutter 完全一样的思路
items.map((item) => Item(key: ValueKey(item.id)))
14. TextField 与 Controller
为什么需要 Controller?
React 用 useState 双向绑定,Flutter 的 TextField 本身不持有状态,需要 TextEditingController 来读写值:
// React
const [phone, setPhone] = useState('')
<input value={phone} onChange={e => setPhone(e.target.value)} />
// Flutter 等价
final _phoneController = TextEditingController()
TextField(controller: _phoneController)
_phoneController.text // 读取值
_phoneController.text = '' // 设置/清空值
_phoneController.addListener(() { ... }) // 监听变化
每个 TextField 对应一个 Controller,Controller 必须在 dispose 里手动释放。
15. dispose 机制
为什么需要 dispose?
Controller 在内存里持有资源(监听器、流、动画帧),页面销毁时 Flutter 不会自动释放,必须手动调用,否则内存泄漏。
需要 dispose 的对象
@override
void dispose() {
_textController.dispose() // TextEditingController
_scrollController.dispose() // ScrollController
_pageController.dispose() // PageController
_animationController.dispose() // AnimationController
_focusNode.dispose() // FocusNode
_timer.cancel() // Timer(用 cancel)
_subscription.cancel() // StreamSubscription(用 cancel)
super.dispose() // 最后交还父类
}
不需要 dispose 的对象
// 普通 Dart 基础类型,GC 自动回收
int _count = 0;
String _name = '';
bool _isLoading = false;
List<String> _items = [];
super 调用顺序
@override
void initState() {
super.initState() // ← 第一行,先让父类初始化
_controller = TextEditingController()
}
@override
void dispose() {
_controller.dispose() // ← 先清理自己
super.dispose() // ← 最后交还父类
}
| 钩子 | super 放哪里 |
|---|---|
initState | 第一行 |
dispose | 最后一行 |
didUpdateWidget | 第一行 |
与 React 对比
| React | Flutter | |
|---|---|---|
| 输入值绑定 | useState 双向绑定 | TextEditingController |
| 清理时机 | useEffect return 函数 | dispose() |
| 自动清理 | Hooks 自动管理 | 必须手动 dispose |
判断口诀
基础类型(int / String / bool / List)→ 不需要
Flutter 控制器 / 监听器 / 定时器 → 必须 dispose