Flutter / Dart 语言基础与开发实际

47 阅读9分钟

Flutter / Dart 学习笔记

目录

Dart 语言基础

  1. const 关键字
  2. Dart 命名规范
  3. 访问控制:公开与私有
  4. 类的标准结构
  5. final / const / var / late
  6. Null Safety 操作符
  7. 常用集合操作
  8. async / await 异步
  9. Dart 3.0 新特性
  10. 集合字面量控制流

Flutter 开发实践

  1. 常用函数类型
  2. BuildContext 是什么
  3. Key 的使用场景
  4. TextField 与 Controller
  5. dispose 机制
  6. 集合字面量控制流
  7. TextField 与 Controller
  8. dispose 机制

1. const 关键字

什么时候可以加 const?

const 加在编译期就能确定值的地方,需要同时满足:

  1. 构造函数本身声明了 const
  2. 所有参数都是编译期常量(字面量、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 命名规范

命名风格

场景风格例子
变量、方法、参数camelCaseuserName, onTap
类、枚举、类型PascalCaseMyWidget, ColorScheme
常量camelCase(不用全大写)const defaultPadding = 8.0
文件名snake_cased04.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_MyClassMyClass
属性 / 变量_countcount
顶层常量_kSpacingkSpacing
枚举_TabIndexTabIndex
抽象类_BaseItemBaseItem
// 顶层常量
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 clauseswitch 分支加条件

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 是什么

mountedState 的属性,表示当前组件是否还在组件树中:

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 对比

ReactFlutter
输入值绑定useState 双向绑定TextEditingController
清理时机useEffect return 函数dispose()
自动清理Hooks 自动管理必须手动 dispose

判断口诀

基础类型(int / String / bool / List)→ 不需要

Flutter 控制器 / 监听器 / 定时器 → 必须 dispose