Skip to content

01 · Dart 语言基础

目标:读完后能看懂 lib/ 下任意一个文件的语法,不再被 ?!..switch 表达式卡住。

本文所有「项目实例」都标注了 文件:行号,可直接跳转核对。


开篇:Dart 与 TypeScript 的整体对照

Dart 和 TS 非常像——都是 C 系语法、都有类型推断、都支持 async/await。主要差异集中在空安全模式匹配两块。

概念TypeScriptDart
变量声明const / letfinal(一次赋值)/ var(可改)/const(编译时常量)
类型推断const x = 1final x = 1推荐显式写类型
可空类型string | nullString?
非空断言x!x!
可选链a?.ba?.b
空值合并a ?? ba ?? b
接口interface Foo {}abstract class Foo {}@freezed class
联合类型type R = A | Bsealed class R(配合模式匹配)
异步Promise<T>Future<T>
异步等待awaitawait
数组Array<T> / T[]List<T>
键值对Record<string, T> / MapMap<String, T>
解构const {a, b} = objfinal (a, b) = record(Dart 3 Record)
展开运算符[...a, ...b][...a, ...b](一样)
箭头函数() => {}() {}() => expr
泛型Foo<T>Foo<T>(一样)

一、变量:final / var / const 的三选一

前端对照

TSDart
const x = 1(不可重新赋值)final x = 1(运行时确定,赋值一次)
let x = 1(可改)var x = 1(可改)
const enum / 字面量常量const x = 1编译时常量)

三条规则

  1. 绝大多数情况用 final —— 变量只赋值一次,运行时计算
  2. 只有需要重新赋值才用 var —— 但 Widget 里几乎不需要(见 03 章「为什么 build 里不用 var」)
  3. const 是编译期常量 —— 它的值必须在编译时就确定
dart
final now = DateTime.now();      // ✅ 运行时计算,final 可以
const now = DateTime.now();     // ❌ 编译错误!DateTime.now() 不是编译期常量

const pi = 3.14159;             // ✅ 字面量,编译期确定
const list = [1, 2, 3];         // ✅ 常量字面量

项目实例

lib/ui/core/models/paged_state.dart:28 —— freezed 类的私有构造用 const

dart
const PagedState._();

lib/ui/core/notifiers/paged_notifier_mixin.dart:67 —— 状态赋值用 const

dart
state = const AsyncValue.loading();

这里 AsyncValue.loading()const性能关键:同一个常量对象在整个 App 里只创建一次。详见 03 章。

⚠️ 本项目最高频的坑:不要用 const 包 token

dart
padding: const EdgeInsets.all(AppTokens.spacingMd)   // ❌ 编译错误
padding: EdgeInsets.all(AppTokens.spacingMd)          // ✅

原因:AppTokens.spacingMd 是一个 getter(运行时从主题取),不是编译期常量。所有 AppTokens.* 都不能进 const 上下文。

练习 1.1

lib/main_playground.dart 里试:

dart
// 依次取消注释,观察哪个报错、报什么错
const a = 1 + 2;              // ?
const b = DateTime.now();     // ?
final c = DateTime.now();     // ?
var d = 1;                    // ?
d = 2;                        // ?
final e = 1;
e = 2;                        // ?

自检

  • [ ] 能说出 finalconst 的区别(运行时 vs 编译期)
  • [ ] 知道为什么 const EdgeInsets.all(AppTokens.spacingMd) 会报错
  • [ ] 在自己的代码里,默认用 final

二、空安全:? / ! / late / ?? / ?.

Dart 的健全空安全(sound null safety)是它比 TS 严格的地方:TS 的 strictNullChecks 只影响类型检查,Dart 的空安全是运行时也保证的。

前端对照

TSDart含义
string | nullString?可空类型
x!x!非空断言(我保证不为 null)
x?.yx?.y可空访问
x ?? defaultx ?? default空值合并
if (x !== null)if (x != null)判空后自动提升为非空
late String x;延迟初始化(我保证用之前会赋值)
required String x命名参数必填

四个符号

dart
String? name;          // 可空:name 可能是 null
name!.length;          // 断言非空:如果 name 是 null 会运行时抛错
name?.length;          // 安全访问:name 为 null 时整个表达式是 null
name ?? '匿名';        // 空值合并:name 为 null 时用默认值

「类型提升」——Dart 比 TS 更聪明的地方

dart
void printLength(String? s) {
  if (s != null) {
    // 这里 s 已经被**提升**为 String(非空),不需要 s!
    print(s.length);   // ✅
  }
}

这在 TS 里也成立,但 Dart 的提升范围更广,包括 return 提前退出后:

dart
String describe(String? s) {
  if (s == null) return 'null';
  return '长度 ${s.length}';   // ✅ s 已提升
}

late:延迟初始化

dart
late String _token;   // 声明时不赋值,承诺用之前会赋值

void init() => _token = 'abc';
void use() => print(_token.length);   // 如果没调 init 就 use → LateInitializationError

本项目中的典型用法StatefulWidget 里的 controller(见 03 章生命周期)。

什么时候不该用 late:如果一个值本来就可能没有,用 String? 而不是 late Stringlate 是「我保证有」的承诺,不是「可能是 null」的表达。

required:命名参数必填

dart
// 定义
void login({required String username, required String password}) {}

// 调用
login(username: 'a', password: 'b');   // ✅
login(username: 'a');                   // ❌ 编译错误

项目实例

lib/ui/core/models/paged_state.dart:19-26 —— freezed 模型里可空与默认值的配合:

dart
const factory PagedState({
  @Default([]) List<T> items,      // 默认空列表,非 null
  @Default(1) int page,
  @Default(true) bool hasMore,
  Object? loadMoreError,            // 可空:null 表示没有错误
  int? totalCount,                  // 可空:后端可能不返回
}) = _PagedState<T>;

注意这里的设计意图:loadMoreErrorObject? 而不是 bool hasError,这样错误信息本身也被保留。

lib/ui/core/notifiers/paged_notifier_mixin.dart:98-99 —— 判空提升:

dart
final current = state.valueOrNull;
if (current == null) return;        // 提前返回
// 此后 current 是非空的 PagedState<TOrder>
state = AsyncValue.data(current.copyWith(isFetchingMore: true));

练习 1.2

dart
String? maybe;

void main() {
  // 1. 不判空直接用 maybe.length,看报什么错(编译期还是运行期?)
  // 2. 用 ?. / ?? / ! 分别改写,观察区别
  // 3. 写一个函数 describe(List<String>? items),
  //    要求:null 返回 '无',空列表返回 '空',否则返回 '共 N 项'
}

自检

  • [ ] 看到 String? 立刻知道要判空或用 ?. / ??
  • [ ] 知道 late 用错会抛 LateInitializationError
  • [ ] 能解释 PagedState 里为什么 loadMoreErrorObject? 而不是 bool

三、异步:Future / async / await

前端对照

TSDart
Promise<T>Future<T>
async functionFuture<T> func() async
awaitawait
Promise.all([a, b])Future.wait([a, b])
Promise.resolve(x)Future.value(x)
new Promise(r => setTimeout(r, 100))Future.delayed(Duration(milliseconds: 100))
try/catchtry/catch(一样)
Observable / EventEmitterStream<T>(见 06 章)

语法几乎一模一样,主要差异:

dart
// Dart:返回类型要写 Future<T>
Future<String> fetchName() async {
  await Future.delayed(const Duration(seconds: 1));
  return 'Haier';
}

// 不需要 async 也可以返回 Future
Future<String> fetchNameSync() => Future.value('Haier');

项目实例:Result 模式匹配

lib/ui/core/notifiers/paged_notifier_mixin.dart:125-149 是本项目异步 + 模式匹配的标准写法:

dart
Future<PagedState<TOrder>> _fetchPage(int page, {CancelToken? cancelToken}) async {
  final result = await fetchPage(page, keyword: _keyword, ...);
  switch (result) {
    case Ok(value: final pageDto):
      _totalPages = pageDto.totalPages;
      final items = content.map(mapDto).toList();
      return PagedState(items: items, page: page, hasMore: hasMore, ...);
    case ErrorResult(error: final e):
      throw e;      // 抛出后由 AsyncValue.guard 捕获成 error 态
  }
}

注意这个模式的三层:

Future<Result<T>>   ← Repository 返回(不抛异常,把错误包进 Result)
      ↓ switch 模式匹配
  Ok  → 取 .value 用
  Error → throw e(转成 Riverpod 的 AsyncValue.error,见 08 章)

练习 1.3

dart
Future<int> slowAdd(int a, int b) async {
  await Future.delayed(const Duration(milliseconds: 500));
  return a + b;
}

void main() async {
  // 1. 顺序 await:总共耗时多少?
  final x = await slowAdd(1, 2);
  final y = await slowAdd(3, 4);
  
  // 2. 并发:改成 Future.wait,耗时多少?
  final results = await Future.wait([slowAdd(1, 2), slowAdd(3, 4)]);
}

自检

  • [ ] 能写出 Future<T> func() async 的签名
  • [ ] 知道 Future.wait 是并发,顺序 await 是串行
  • [ ] 能读懂 switch (result) { case Ok(...): case ErrorResult(...): }

四、集合操作:map / where / fold / expand

前端对照

Dart 的 Iterable API 和 JS 的 Array 几乎一一对应

JSDart
arr.map(f)list.map(f)(返回懒加载的 Iterable,要 .toList()
arr.filter(f)list.where(f)
arr.reduce(f, init)list.fold(init, f)
arr.flatMap(f)list.expand(f)
arr.find(f)list.firstWhere(f, orElse: () => default)
arr.some(f)list.any(f)
arr.every(f)list.every(f)
arr.forEach(f)list.forEach(f)
[...a, ...b][...a, ...b]
arr.lengthlist.length

⚠️ 差异一:.map() 返回的是 Iterable,不是 List

dart
final list = [1, 2, 3];
final doubled = list.map((x) => x * 2);      // Iterable<int>,懒加载
final doubledList = list.map((x) => x * 2).toList();   // List<int>

忘了 .toList() 是新手高频错误。如果函数签名要 List<T>,传 Iterable<T> 会编译失败。

⚠️ 差异二:firstWhere 找不到会抛异常

dart
// JS:arr.find() 找不到返回 undefined
// Dart:firstWhere 找不到会抛 StateError,除非给 orElse
final x = list.firstWhere((e) => e.id == 1, orElse: () => defaultItem);

项目实例

lib/ui/core/notifiers/paged_notifier_mixin.dart:136-139map + toList):

dart
final content = pageDto.content;
final items = content.map(mapDto).toList();

lib/ui/core/notifiers/paged_notifier_mixin.dart:105-108(展开符合并两页数据):

dart
final merged = <TOrder>[
  ...current.items,     // 已加载的
  ...next.items,        // 新一页
];

lib/ui/auth/models/auth_state.dart:172any 判断权限):

dart
return perms.any((p) => p == '*:*:*' || p == code);

这行实现的是「超管通配 *:*:* 或精确匹配」。

练习 1.4

dart
final users = [
  {'name': '张三', 'age': 20, 'city': '青岛'},
  {'name': '李四', 'age': 17, 'city': '青岛'},
  {'name': '王五', 'age': 25, 'city': '北京'},
];

// 1. 找出所有青岛人的名字(List<String>)
// 2. 判断是否有人未成年(bool)
// 3. 求所有人年龄之和(int,用 fold)

自检

  • [ ] 记得 .map() 后要 .toList()
  • [ ] firstWhere 会写 orElse
  • [ ] 会用 ... 展开符合并列表

五、级联操作符 ..?..

这是 JS 里没有的语法,也是 Dart 代码里非常常见的写法。

概念

.. 让你对同一个对象连续调用多个方法/设置多个属性,而不用重复写对象名。

dart
// 不用级联
final p = Person();
p.name = '张三';
p.age = 20;
p.sayHello();

// 用级联
final p = Person()
  ..name = '张三'
  ..age = 20
  ..sayHello();

关键:级联表达式的返回值是对象本身,不是最后一个方法的返回值。

dart
final x = obj..method();   // x === obj(不是 method() 的返回值)

前端类比

最接近的是 jQuery 链式调用,或 JS 的 Object.assign

js
// JS
const config = Object.assign(new Config(), { a: 1, b: 2 });

// Dart
final config = Config()
  ..a = 1
  ..b = 2;

?.. 可空级联

对象可能为 null 时用 ?..,为 null 时整条链跳过:

dart
controller?..setText('x')..update();   // controller 为 null 时什么都不做

项目实例

lib/main.dart:42-57 —— EasyLoading 配置,级联的经典用法:

dart
EasyLoading.instance
  ..displayDuration = const Duration(milliseconds: 2000)
  ..animationDuration = const Duration(milliseconds: 200)
  ..maskType = EasyLoadingMaskType.none
  ..userInteractions = false
  ..dismissOnTap = false
  ..loadingStyle = EasyLoadingStyle.custom
  ..backgroundColor = theme.barrier
  ..textColor = Colors.white
  ..indicatorColor = Colors.white
  ..fontSize = AppTokens.bodyMdRegular.fontSize!
  ..radius = AppTokens.radiusLarge
  ..contentPadding = EdgeInsets.symmetric(
      horizontal: AppTokens.spacingSm,
      vertical: AppTokens.spacingXs,
    );

如果不用级联,这段代码要写 12 遍 EasyLoading.instance

练习 1.5

用级联改写下面这段(不用级联的版本):

dart
final sb = StringBuffer();
sb.write('Hello');
sb.write(' ');
sb.write('Flutter');
print(sb.toString());

自检

  • [ ] 看到 x..a..b..c 知道是对同一个 x 连续操作
  • [ ] 知道级联表达式返回对象本身
  • [ ] 能读懂 main.dart 里 EasyLoading 的配置

六、闭包与 typedef

前端对照

Dart 的闭包(匿名函数)和 JS 箭头函数几乎一样:

dart
final f = (int x) => x * 2;          // 类似 JS: (x) => x * 2
list.map((item) => item.name);        // 类似 JS: list.map(item => item.name)

差异:Dart 的函数必须声明参数类型(或在上下文可推断时省略)。

typedef:给函数类型起别名

dart
typedef ItemBuilder<T> = Widget Function(BuildContext context, T item, int index);

之后就可以用 ItemBuilder<T> 代替那一长串。

项目实例

lib/ui/core/notifiers/paged_notifier_mixin.dart:51 —— 抽象方法作为「回调」交给子类实现:

dart
/// 子类提供:DTO → UI model 映射。
TOrder mapDto(TDto dto);

这不是 typedef,但思想一致:把「怎么转换」当作参数传递。使用时:

dart
final items = content.map(mapDto).toList();   // mapDto 作为函数传入 map

lib/ui/core/widgets/paged_list_body.dart:43 —— 典型的 Widget builder 回调签名:

dart
Widget itemBuilder(BuildContext context, T item, int index);

练习 1.6

dart
// 1. 写一个 typedef:typedef Transformer<T, R> = R Function(T input);
// 2. 写一个函数 applyAll<T,R>(List<T> items, Transformer<T,R> f) => items.map(f).toList();
// 3. 用它把 [1,2,3] 转成 ['1','2','3']

自检

  • [ ] 能写 (x) => expr 形式的闭包
  • [ ] 看到 typedef X = Y Function(Z) 知道是函数类型别名
  • [ ] 能读懂 itemBuilder(BuildContext, T, int) 这种抽象方法的含义

七、Record 与模式匹配(Dart 3)

这是 Dart 3 最重要的新特性,也是本项目代码里大量使用的。

前端对照

概念TSDart 3
元组[string, number](String, int) —— Record
具名字段元组{ name: string; age: number }({String name, int age}) —— Record
解构const [a, b] = tfinal (a, b) = t
switch 表达式无(switch 是语句)switch表达式,可返回值
穷尽性检查需手动 never 兜底sealed class 自动检查

Record:轻量多元组

dart
(String, int) getUser() => ('张三', 20);

final (name, age) = getUser();       // 解构
print('$name 今年 $age 岁');

// 具名字段版本
({String name, int age}) getUser2() => (name: '张三', age: 20);
final u = getUser2();
print(u.name);

什么时候用 Record 而不是 class:临时返回 2~3 个值、不值得建一个类的场合。本项目中较少使用(因为大量用 freezed 类),但第三方库和 Flutter 新 API 里很常见。

模式匹配:switch 表达式

Dart 3 的 switch 是表达式,可以返回值

dart
final desc = switch (statusCode) {
  200 => '成功',
  404 => '未找到',
  _   => '未知',       // _ 是通配符(类似 default)
};

解构匹配——这是本项目用得最多的形式:

dart
final result = await repo.fetch();
return switch (result) {
  Ok(value: final data) => processData(data),   // 匹配 Ok 并取出 value
  ErrorResult(error: final e) => throw e,        // 匹配 Error 并取出 error
};

对比传统写法:

dart
// 不用模式匹配(啰嗦且需要类型断言)
if (result is Ok) {
  return processData((result as Ok).value);
} else if (result is ErrorResult) {
  throw (result as ErrorResult).error;
}

sealed class + 穷尽性检查

sealed 类的子类必须在同一个文件里,编译器因此知道所有可能的子类型,可以检查你的 switch 是否遗漏分支。

dart
@freezed
sealed class Result<T> with _$Result<T> {
  const factory Result.ok(T value) = Ok<T>;
  const factory Result.error(Exception error) = ErrorResult<T>;
}

因为没有 default 分支,如果以后有人给 Result 加了第三个变体,所有漏改的 switch 都会编译报错——这是非常强的重构保护。

项目实例

lib/ui/core/notifiers/paged_notifier_mixin.dart:135-148(已见):

dart
switch (result) {
  case Ok(value: final pageDto):
    ...
  case ErrorResult(error: final e):
    throw e;
}

lib/main.dart:155-161 —— 对枚举的模式匹配:

dart
final isDark = switch (mode) {
  ThemeMode.dark => true,
  ThemeMode.light => false,
  ThemeMode.system =>
    View.of(context).platformDispatcher.platformBrightness == Brightness.dark,
};

lib/core/permission/permission_guard.dart:25-50 —— 对枚举穷举匹配(省略 default,靠编译器检查):

dart
switch (permission) {
  case AppPermission.location:
    return PermissionRationale(...);
  case AppPermission.camera:
    return PermissionRationale(...);
  case AppPermission.photos:
    return PermissionRationale(...);
  case AppPermission.notification:
    return PermissionRationale(...);
}

练习 1.7

dart
sealed class Shape {}
class Circle extends Shape { final double r; Circle(this.r); }
class Rect extends Shape { final double w, h; Rect(this.w, this.h); }

// 1. 用 switch 表达式写 area(Shape s),不写 default 分支
// 2. 再加一个 Triangle 子类,观察编译器报什么错(体验穷尽性检查)

自检

  • [ ] 知道 switch 在 Dart 3 里是表达式
  • [ ] 能读懂 Ok(value: final data) 这种解构匹配
  • [ ] 理解 sealed class 带来的编译期穷尽性保护

八、@freezed 数据类(先看概念,细节见 09 章)

项目里所有模型都用 freezed 代码生成。你只需要知道:

dart
@freezed
class PagedState<T> with _$PagedState<T> {
  const factory PagedState({
    @Default([]) List<T> items,
    @Default(1) int page,
    Object? loadMoreError,
  }) = _PagedState<T>;
}

它自动生成:

  • copyWith() —— 复制并修改部分字段(不可变对象的关键
  • == / hashCode / toString() —— 值相等比较
  • 不可变性(所有字段 final

前端对照

前端freezed
TypeScript interface数据结构的定义
immer 的 producecopyWith
Object.freeze天然不可变
dart
// 修改状态:不是改字段,而是产生新对象
final next = current.copyWith(isFetchingMore: true);

为什么不可变很重要:Riverpod 通过比较对象引用(==)判断是否需要重建 UI。可变对象会让这个判断失效。详见 08 章。

生成命令

写完/改完 @freezed@riverpod 的类后必须跑:

bash
fvm dart run build_runner build --delete-conflicting-outputs

生成的 *.freezed.dart / *.g.dart 要提交到 git

自检

  • [ ] 知道 copyWith 是产生新对象,不是原地修改
  • [ ] 记得改模型后要跑 build_runner

九、本章速查

想做的事写法
声明不可重新赋值的变量final x = ...
声明编译期常量const x = ...
可空类型String?
断言非空x!
安全访问x?.y
空值兜底x ?? default
延迟初始化late String x;
命名参数必填{required String x}
异步函数Future<T> f() async {}
并发等待Future.wait([a, b])
集合转换list.map(f).toList()
条件过滤list.where(f).toList()
聚合list.fold(init, (acc, e) => ...)
合并列表[...a, ...b]
连续操作同一对象obj..a = 1..b = 2
switch 表达式final x = switch (v) { A => 1, _ => 2 };
解构匹配case Ok(value: final d):
复制对象obj.copyWith(field: v)

下一步

02 · Flutter 核心基础