利用 Dart 3 的现代语言特性提升代码的表达力与安全性


目录

  1. 引言:Dart 3 带来的范式转变
  2. Records:告别临时数据类
  3. Patterns:解构世界的全新语法
  4. Sealed Classes:代数数据类型的 Dart 实现
  5. 实战一:MoodStorage 返回值用 Record 重构
  6. 实战二:情绪过滤逻辑用 Pattern Matching 重写
  7. 实战三:状态管理用 Sealed Class 建模
  8. 实战四:综合重构——DiaryPage 的完整改造
  9. 代码量对比与收益分析
  10. 鸿蒙平台兼容性说明
  11. 迁移策略与注意事项
  12. 总结

一、引言:Dart 3 带来的范式转变

2023 年 5 月,Dart 3 正式发布。这是 Dart 语言历史上最大的一次版本更新,引入了 Records、Patterns、Sealed Classes 三大核心特性,以及 Class Modifiers、Switch Expressions 增强等多项改进。对于 Flutter 开发者而言,这些特性不仅仅是语法糖——它们从根本上改变了我们组织代码的方式。

在 Dart 3 之前,我们写 Flutter 代码时常面临几个痛点:

  • 函数需要返回多个值:要么创建一个只有两个字段的类,要么用 Map 凑合,要么依赖第三方库的 Tuple
  • 状态建模不够严谨:用 enum 表示状态,但每个状态可能携带不同的数据(如加载中需要携带进度、成功需要携带数据、失败需要携带错误信息),这导致了大量 if 检查和空安全断言。
  • JSON 解析与解构繁琐:从 Map<String, dynamic> 中逐层取出值,代码冗长且容易出错。
  • switch 语句表达能力弱:无法在 case 中附加条件,无法对复杂数据结构进行匹配。

Dart 3 的三大特性恰好解决了这些问题。在 E-Brufen 项目(一个面向鸿蒙平台的情绪健康 Flutter 应用)中,我们逐步将这些新特性引入,取得了显著的效果:代码行数减少约 30%,类型安全性大幅提升,bug 数量明显下降。

让我们从这三个特性逐一深入,看看它们如何在真实项目中发挥作用。


二、Records:告别临时数据类

在这里插入图片描述

2.1 传统方案的痛点

在 E-Brufen 项目中,我们经常需要从 MoodStorage 中查询数据并返回多个相关的值。例如,查询某天的情绪记录时,不仅需要返回记录列表,还需要返回当天的平均情绪值和高频情绪类型。

在 Dart 3 之前,我们有三种选择:

方案 A:创建专用 Data Class

// 传统方案:为一次查询创建专用类
class DailyMoodResult {
  final List<MoodEntry> entries;
  final double averageValue;
  final MoodType dominantMood;

  const DailyMoodResult({
    required this.entries,
    required this.averageValue,
    required this.dominantMood,
  });
}

// 使用
DailyMoodResult result = moodStorage.getDailyReport(date);

问题:一个类只有一处使用,却要占用 10 行以上的定义代码。项目中的临时数据类会迅速膨胀。

方案 B:使用 Map

Map<String, dynamic> getDailyReport(DateTime date) {
  // ...
  return {
    'entries': entries,
    'averageValue': avg,
    'dominantMood': dominant,
  };
}

// 使用时失去类型安全
final result = moodStorage.getDailyReport(date);
final entries = result['entries'] as List<MoodEntry>; // 强制转型!
final avg = result['averageValue'] as double;          // 运行时可能崩溃

问题:完全丧失了类型安全,IDE 无法提供自动补全,重构时容易遗漏。

方案 C:使用第三方 Tuple 库

// 引入 package:tuple
Tuple3<List<MoodEntry>, double, MoodType> getDailyReport(DateTime date);

问题:增加依赖,且 tuple.item1tuple.item2 这种命名毫无语义。

2.2 Record 的优雅解法

Dart 3 的 Record 是一种匿名的、不可变的、结构类型的复合数据类型。它的语法简洁,且天然支持类型推断和解构。

// Dart 3 Record 方案
(List<MoodEntry>, double, MoodType) getDailyReport(DateTime date) {
  final entries = getByDate(date);
  final avg = entries.isEmpty
      ? 0.0
      : entries.map((e) => e.moodType.value).reduce((a, b) => a + b) / entries.length;
  final dominant = _findDominantMood(entries);
  return (entries, avg, dominant);
}

// 使用——支持位置解构
final (entries, avg, dominant) = moodStorage.getDailyReport(date);
print('共 ${entries.length} 条记录,平均情绪值 $avg');

Record 还支持命名字段,进一步提升可读性:

// 命名 Record——语义更清晰
({List<MoodEntry> entries, double average, MoodType dominant}) getDailyReport(
    DateTime date) {
  final entries = getByDate(date);
  final avg = entries.isEmpty
      ? 0.0
      : entries.map((e) => e.moodType.value).reduce((a, b) => a + b) / entries.length;
  final dominant = _findDominantMood(entries);
  return (entries: entries, average: avg, dominant: dominant);
}

// 使用命名解构
final (:entries, :average, :dominant) = moodStorage.getDailyReport(date);
print('${dominant.label} 是最主要的情绪,平均分 $average');

2.3 Record 的核心特性

特性说明示例
匿名性不需要声明类名(int, String)
不可变性Record 创建后字段不可修改final r = (1, 'a'); ——不能 r.$1 = 2
结构等价字段类型相同即类型相同(int, String) 在任何位置都是同一类型
位置访问通过 $1, $2, ... 访问record.$1, record.$2
命名访问通过字段名访问record.entries, record.average
解构支持配合 Patterns 一键拆解final (a, b) = record;
判等结构判等,非引用判等(1, 'a') == (1, 'a')true
hashCode自动生成可用于 Map 的 key

2.4 何时使用 Record vs 何时使用 Class

Record 不是银弹。以下决策矩阵可以帮助我们做出选择:

场景推荐方案理由
函数返回多个临时值Record无需创建额外类,语义清晰
数据需要跨多个模块共享Class需要明确的类型名和文档
需要可变状态ClassRecord 不可变
需要 JSON 序列化ClassRecord 不支持 toJson()
作为 Map 的 keyRecord自动 hashCode,结构判等
需要自定义方法ClassRecord 不能定义方法

三、Patterns:解构世界的全新语法

3.1 什么是 Pattern Matching

Pattern Matching 不仅仅是 switch 的增强版。它是一种声明式的数据结构匹配与解构机制,允许我们同时完成"判断数据结构"和"提取内部值"两件事。

Dart 3 的 Patterns 可以在以下场景使用:

  • 变量声明(解构)
  • 变量赋值
  • switch 语句和表达式
  • if-case 语句
  • for-in 循环
  • 集合字面量

3.2 基础解构

List 解构:

// Dart 3 之前
final list = [1, 2, 3];
final a = list[0];
final b = list[1];
final c = list[2];

// Dart 3
final [a, b, c] = [1, 2, 3];
print('$a, $b, $c'); // 1, 2, 3

// 结合 rest element 捕获剩余元素
final [first, ...rest, last] = [1, 2, 3, 4, 5];
print(first); // 1
print(rest);  // [2, 3, 4]
print(last);  // 5

Map 解构:

final json = {'name': 'E-Brufen', 'version': '1.0.0', 'platform': 'HarmonyOS'};

// Dart 3 Map 解构
final {'name': name, 'version': version} = json;
print('$name v$version'); // E-Brufen v1.0.0

Object 解构:

// 解构 MoodEntry 的字段
final mood = MoodEntry(
  moodType: MoodType.happy,
  note: '今天完成了 Dart 3 重构',
  createdAt: DateTime.now(),
  updatedAt: DateTime.now(),
);

// 使用 getter 名称进行对象解构
final MoodEntry(:moodType, :note, :createdAt) = mood;
print('${moodType.label}: $note');

3.3 Switch Expression 的全面增强

Dart 3 的 switch 表达式支持对任意类型进行模式匹配,并且可以在 case 中附加 when 条件。这是整个 Patterns 体系中实战价值最高的特性。

示例:根据情绪值生成建议文案

在 E-Brufen 的 MoodChart 组件中,我们原本使用 if-else 链来判断情绪等级:

// Dart 3 之前的实现
String getMoodAdvice(double averageValue) {
  if (averageValue >= 4.0) {
    return '这周心情很阳光!继续保持 🌞';
  } else if (averageValue >= 3.0) {
    return '这周心情平稳,一切刚刚好 🍃';
  } else if (averageValue >= 2.0) {
    return '这周有些低落,给自己一个拥抱 🫂';
  } else if (averageValue > 0) {
    return '这周辛苦了,好好休息一下吧 💤';
  } else {
    return '本周还没有记录哦,来记录第一份心情吧 🌱';
  }
}

使用 Dart 3 的 switch 表达式配合 when 条件守卫:

// Dart 3 switch expression + when guard
String getMoodAdvice(double averageValue) => switch (averageValue) {
      >= 4.0 => '这周心情很阳光!继续保持 🌞',
      >= 3.0 => '这周心情平稳,一切刚刚好 🍃',
      >= 2.0 => '这周有些低落,给自己一个拥抱 🫂',
      > 0    => '这周辛苦了,好好休息一下吧 💤',
      _      => '本周还没有记录哦,来记录第一份心情吧 🌱',
    };

这个版本代码行数减少 56%(从 12 行降到约 7 行),且逻辑更加一目了然。

示例:根据情绪类型和时间段生成复合建议

这是一个更复杂的场景。我们不仅需要根据情绪类型(happy、calm、sad 等)给出建议,还需要结合时间段(早上、下午、晚上):

// Dart 3: 嵌套 switch 在单个表达式中完成
String getContextualAdvice(MoodType mood, int hour) => switch ((mood, hour)) {
      (MoodType.happy, >= 6 && < 12) => '早晨的好心情是全天最好的礼物 ☀️',
      (MoodType.happy, >= 12 && < 18) => '下午的愉悦感让工作更高效 ✨',
      (MoodType.happy, _)              => '带着快乐入睡,晚安 🌙',
      (MoodType.sad, >= 18)            => '夜晚容易伤感,听一首温暖的歌吧 🎵',
      (MoodType.sad, _)                => '今天有些难过,给自己泡杯热茶 🍵',
      (MoodType.tired, _)              => '疲惫是身体在提醒你需要休息 🛌',
      (MoodType.angry, _)              => '深呼吸,愤怒过后是平静 🫁',
      _                                => '每一天都值得被温柔对待 🌿',
    };

注意这里的关键技巧:我们用 (mood, hour) 创建了一个临时 Record,然后在 switch 中对这个 Record 进行模式匹配。每一个 case 同时匹配了 MoodTypeint 两个维度,实现了二维模式匹配

3.4 if-case 语句

if-case 是 Dart 3 引入的另一种模式匹配方式,适合只有一两个分支需要特殊处理的场景:

// Dart 3 之前:需要先转换类型再判断
void processMoodData(dynamic data) {
  if (data is List<MoodEntry>) {
    if (data.isNotEmpty) {
      // 处理...
    }
  }
}

// Dart 3: if-case 一行完成类型判断 + 解构
void processMoodData(dynamic data) {
  if (data case [MoodEntry first, ...]) {
    // data 被自动转型为 List<MoodEntry>,且 first 可直接使用
    print('最新的记录: ${first.moodType.label}');
  }
}

四、Sealed Classes:代数数据类型的 Dart 实现

4.1 什么是代数数据类型

代数数据类型(Algebraic Data Type,ADT)是函数式编程中的核心概念。简单来说,一个 ADT 是由有限个确定的子类型组成的封闭类型。在 Dart 3 中,sealed class 实现了这一点。

ADT 最经典的应用场景是状态建模。任何异步操作的状态都可以用 ADT 精确表达:

异步操作状态 = 初始状态(Idle)
            | 加载中(Loading)
            | 成功(Success,携带数据)
            | 失败(Failure,携带错误信息)

sealed class 的关键约束是:所有子类必须在同一个文件中定义。这保证了编译器知道所有的子类型,从而在 switch 表达式中实现穷尽性检查(exhaustiveness check)——如果你遗漏了某个子类型,编译器会直接报错。

4.2 E-Brufen 中的状态建模

在 E-Brufen 项目中,情绪日记页有多个异步操作:加载记录、保存记录、删除记录。在 Dart 3 之前,我们使用一个 enum 加上若干可空字段来表示状态:

// ── Dart 3 之前:使用 enum + 可空字段 ──
enum DiaryStatus { idle, loading, success, error }

class DiaryState {
  final DiaryStatus status;
  final List<MoodEntry>? entries;
  final String? errorMessage;
  final bool? isSaving;

  const DiaryState({
    this.status = DiaryStatus.idle,
    this.entries,
    this.errorMessage,
    this.isSaving,
  });
}

这种模式有三个严重问题:

  1. 非法状态可达:当 status == DiaryStatus.success 时,entries 可能为 nullerrorMessage 也可能有值——编译器不会阻止这种不一致的状态。
  2. 空安全检查泛滥:每次使用 entries 都需要 !??,即使逻辑上确定它不为 null。
  3. 扩展困难:当需要添加新的状态(比如"部分成功"或"离线缓存命中")时,需要修改所有使用该状态的代码。

4.3 Sealed Class 方案

// ── Dart 3 Sealed Class 方案 ──
sealed class DiaryLoadState {
  const DiaryLoadState();
}

class DiaryInitial extends DiaryLoadState {
  const DiaryInitial();
}

class DiaryLoading extends DiaryLoadState {
  const DiaryLoading();
}

class DiaryLoaded extends DiaryLoadState {
  final List<MoodEntry> entries;
  final DateTime? lastSyncTime;
  const DiaryLoaded({required this.entries, this.lastSyncTime});
}

class DiaryEmpty extends DiaryLoadState {
  const DiaryEmpty();
}

class DiaryError extends DiaryLoadState {
  final String message;
  final Object? error;
  const DiaryError({required this.message, this.error});
}

使用 sealed class 后,状态的消费方式发生了根本变化。编译器保证了穷尽性:

Widget buildDiaryContent(DiaryLoadState state) {
  return switch (state) {
    DiaryInitial()  => const SizedBox.shrink(),
    DiaryLoading()  => const Center(child: CircularProgressIndicator()),
    DiaryEmpty()    => const Center(child: Text('还没有记录,来记录第一份心情吧 🌱')),
    DiaryLoaded(:final entries, :final lastSyncTime) =>
      _buildTimelineList(entries, lastSyncTime),
    DiaryError(:final message) =>
      Center(child: Text('加载失败: $message', style: TextStyle(color: Colors.red))),
  };
  // 如果遗漏任何一个子类型,编译器会报错!
}

关键优势:

  • 类型安全DiaryLoaded 中的 entriesList<MoodEntry>,不存在可为 null 的问题——因为 DiaryLoaded 这个类型本身就保证了数据的存在。
  • 穷尽性检查:新增一个子类型后,所有 switch 表达式都会收到编译错误,强制你处理新状态。
  • 零开销抽象:编译后的代码与 enum + if-else 性能相同。

4.4 Sealed Class vs Enum vs 抽象类的对比

维度EnumSealed Class抽象类
是否封闭否(可跨文件继承)
可携带数据有限(仅 enum 字段)是(每个子类独立字段)
穷尽性检查
可定义方法
子类数量编译期固定同文件内任意任意
适合场景简单枚举状态机、API 响应开放扩展

五、实战一:MoodStorage 返回值用 Record 重构

5.1 改造前

查看 E-Brufen 当前的 MoodStorage.getWeeklyMoodCounts() 方法:

// 当前实现:返回 Map<int, int>,含义不明确
Map<int, int> getWeeklyMoodCounts(DateTime anyDay) {
  final rows = getByWeek(anyDay);
  final counts = <int, int>{1: 0, 2: 0, 3: 0, 4: 0, 5: 0};
  for (final r in rows) {
    counts[r.moodType.value] = (counts[r.moodType.value] ?? 0) + 1;
  }
  return counts;
}

调用方需要记住 1=生气, 2=难过, 3=疲惫, 4=平静, 5=开心 这种隐式映射,容易出错。

5.2 改造后

我们新增一个方法,用 Record 返回更丰富的信息:

// ── 新增方法:返回结构化周报数据 ──
({Map<MoodType, int> counts, double average, MoodType dominant, int totalEntries})
    getWeeklyReport(DateTime anyDay) {
  final rows = getByWeek(anyDay);
  final counts = <MoodType, int>{
    for (final mood in MoodType.values) mood: 0,
  };

  for (final r in rows) {
    counts[r.moodType] = (counts[r.moodType] ?? 0) + 1;
  }

  final totalEntries = rows.length;
  final average = totalEntries == 0
      ? 0.0
      : rows.map((e) => e.moodType.value).reduce((a, b) => a + b) / totalEntries;

  final dominant = counts.entries
      .fold<MapEntry<MoodType, int>?>(
          null, (max, e) => max == null || e.value > max.value ? e : max)
      ?.key ??
      MoodType.calm;

  return (
    counts: counts,
    average: average,
    dominant: dominant,
    totalEntries: totalEntries,
  );
}

调用方的代码变得极为简洁:

// Dart 3 调用方式
final (:counts, :average, :dominant, :totalEntries) =
    moodStorage.getWeeklyReport(DateTime.now());

print('本周共记录 $totalEntries 次,主要情绪是 ${dominant.label},平均 $average');

另外,insert 方法的返回值也可以利用 Record 携带更多信息:

// 改造前:只返回 int id
Future<int> insert(MoodEntry entry) async { ... }

// 改造后:返回 (id, isNewRecord)
Future<(int id, bool isNewRecord)> insert(MoodEntry entry) async {
  final id = _nextId++;
  // ... 持久化逻辑
  notifyListeners();
  return (id, true);
}

六、实战二:情绪过滤逻辑用 Pattern Matching 重写

6.1 情绪分类逻辑

E-Brufen 中有多处需要对情绪进行多级分类的逻辑。例如,首页的问候语需要根据时间和最近情绪生成,统计页需要将情绪分为正面/中性/负面三类。

改造前——嵌套 if-else:

String getMoodCategory(MoodType mood) {
  if (mood == MoodType.happy) {
    return '正面';
  } else if (mood == MoodType.calm) {
    return '正面';
  } else if (mood == MoodType.tired) {
    return '中性';
  } else if (mood == MoodType.sad) {
    return '负面';
  } else if (mood == MoodType.angry) {
    return '负面';
  } else {
    return '未知';
  }
}

改造后——switch expression:

String getMoodCategory(MoodType mood) => switch (mood) {
      MoodType.happy || MoodType.calm  => '正面',
      MoodType.tired                    => '中性',
      MoodType.sad || MoodType.angry    => '负面',
    };

注意这里使用了 逻辑或模式(logical-or pattern) MoodType.happy || MoodType.calm,允许多个枚举值共享同一个分支。这在 Dart 3 之前需要靠写出重复的 return 语句来实现。

6.2 情绪过滤与统计

在日记页的时间线 Tab 中,我们可能需要根据用户选择的过滤条件来筛选记录。传统做法是使用 where 回调:

// Dart 3 之前:闭包筛选
List<MoodEntry> filterMoods(List<MoodEntry> all, String filter) {
  if (filter == '正面') {
    return all.where((e) =>
        e.moodType == MoodType.happy || e.moodType == MoodType.calm).toList();
  } else if (filter == '负面') {
    return all.where((e) =>
        e.moodType == MoodType.sad || e.moodType == MoodType.angry).toList();
  } else if (filter == '中性') {
    return all.where((e) => e.moodType == MoodType.tired).toList();
  }
  return all;
}

使用 Dart 3 的 switch 配合模式匹配,可以写出更清晰的过滤器:

// Dart 3:声明式的过滤判断
bool moodMatchesFilter(MoodType mood, String filter) => switch (filter) {
      '正面' => mood case MoodType.happy || MoodType.calm,
      '负面' => mood case MoodType.sad || MoodType.angry,
      '中性' => mood == MoodType.tired,
      _      => true,
    };

// 使用
final filtered = allMoods.where((e) => moodMatchesFilter(e.moodType, '正面')).toList();

6.3 复杂条件匹配

在统计页,我们需要识别"情绪转折"——即连续两条记录情绪值变化超过 2 个单位的情况,这通常意味着用户经历了明显的情绪波动。用 Pattern Matching 实现:

/// 检测情绪波动事件
/// 返回波动记录对列表
List<({MoodEntry before, MoodEntry after, int delta})> detectMoodShifts(
    List<MoodEntry> sortedEntries) {
  final shifts = <({MoodEntry before, MoodEntry after, int delta})>[];

  for (var i = 0; i < sortedEntries.length - 1; i++) {
    final [before, after] = [sortedEntries[i], sortedEntries[i + 1]];
    final delta = after.moodType.value - before.moodType.value;

    switch (delta.abs()) {
      case >= 3:
        shifts.add((before: before, after: after, delta: delta));
      case >= 2 when before.moodType.value <= 2 || after.moodType.value <= 2:
        // 如果其中一条已经是负面情绪,即使 delta 只有 2 也值得关注
        shifts.add((before: before, after: after, delta: delta));
      case _:
        // 小幅波动,不记录
        break;
    }
  }

  return shifts;
}

这里 switch (delta.abs()) 配合 when 守卫,实现了分级阈值 + 条件组合的复杂匹配逻辑。注意 case >= 3:case >= 2 when ... 的语法——这是 Dart 3 独有的关系模式(relational pattern)。


七、实战三:状态管理用 Sealed Class 建模

7.1 日记页的异步状态模型

E-Brufen 的情绪日记页涉及三个异步操作:加载记录保存记录删除记录。每个操作都有不同的状态集。我们用 Sealed Class 来建模:

// ── 日记页完整状态模型 ──

// 数据加载状态
sealed class DiaryLoadState {
  const DiaryLoadState();
}

class DiaryInitial extends DiaryLoadState {
  const DiaryInitial();
}

class DiaryLoading extends DiaryLoadState {
  final double progress; // 0.0 ~ 1.0,用于进度条
  const DiaryLoading({this.progress = 0.0});
}

class DiaryLoaded extends DiaryLoadState {
  final List<MoodEntry> allEntries;
  final List<MoodEntry> weekEntries;
  final DateTime? cacheTimestamp;
  const DiaryLoaded({
    required this.allEntries,
    required this.weekEntries,
    this.cacheTimestamp,
  });
}

class DiaryError extends DiaryLoadState {
  final String message;
  final bool isRetryable;
  const DiaryError({required this.message, this.isRetryable = true});
}

// 保存操作状态
sealed class DiarySaveState {
  const DiarySaveState();
}

class SaveIdle extends DiarySaveState {
  const SaveIdle();
}

class SaveInProgress extends DiarySaveState {
  const SaveInProgress();
}

class SaveSuccess extends DiarySaveState {
  final int savedId;
  const SaveSuccess({required this.savedId});
}

class SaveFailure extends DiarySaveState {
  final String message;
  const SaveFailure({required this.message});
}

7.2 在 Widget 中消费状态

class DiaryPage extends StatefulWidget {
  final MoodStorage moodStorage;
  const DiaryPage({super.key, required this.moodStorage});

  
  State<DiaryPage> createState() => _DiaryPageState();
}

class _DiaryPageState extends State<DiaryPage> {
  DiaryLoadState _loadState = const DiaryInitial();
  DiarySaveState _saveState = const SaveIdle();

  
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    setState(() => _loadState = const DiaryLoading(progress: 0.0));

    try {
      final all = widget.moodStorage.getAll();
      final week = widget.moodStorage.getByWeek(DateTime.now());

      setState(() => _loadState = DiaryLoaded(
        allEntries: all,
        weekEntries: week,
        cacheTimestamp: DateTime.now(),
      ));
    } catch (e) {
      setState(() => _loadState = DiaryError(
        message: e.toString(),
        isRetryable: true,
      ));
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('情绪日记')),
      body: Column(
        children: [
          _buildBodyByState(),
          _buildSaveIndicator(),
        ],
      ),
    );
  }

  Widget _buildBodyByState() {
    return switch (_loadState) {
      DiaryInitial()  => const SizedBox.shrink(),
      DiaryLoading(:final progress) =>
        Center(child: CircularProgressIndicator(value: progress)),
      DiaryLoaded(:final allEntries, :final weekEntries) =>
        Expanded(child: _buildContent(allEntries, weekEntries)),
      DiaryError(:final message, :final isRetryable) =>
        Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('加载失败', style: TextStyle(fontSize: 18, color: Colors.red.shade400)),
              const SizedBox(height: 8),
              Text(message, style: const TextStyle(color: Colors.grey)),
              if (isRetryable) ...[
                const SizedBox(height: 12),
                ElevatedButton(onPressed: _loadData, child: const Text('重试')),
              ],
            ],
          ),
        ),
    };
  }

  Widget _buildSaveIndicator() {
    return switch (_saveState) {
      SaveIdle()        => const SizedBox.shrink(),
      SaveInProgress()  => const LinearProgressIndicator(),
      SaveSuccess(:final savedId) =>
        _buildSnackBarAndReset('保存成功 ✅ (ID: $savedId)'),
      SaveFailure(:final message) =>
        _buildSnackBarAndReset('保存失败: $message'),
    };
  }

  void _buildSnackBarAndReset(String msg) {
    // 在下一个帧重置状态
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) {
        setState(() => _saveState = const SaveIdle());
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(msg), duration: const Duration(seconds: 2)),
        );
      }
    });
  }

  Widget _buildContent(List<MoodEntry> all, List<MoodEntry> week) {
    // ... 现有内容构建逻辑
    return const Placeholder();
  }
}

7.3 穷尽性检查的价值

这个重构的核心收益不在于代码减少(虽然也确实减少了),而在于编译器的穷尽性检查。假设三个月后,产品经理决定增加一个"离线缓存过期"的状态。我们只需要在 DiaryLoadState 的层级中添加:

class DiaryCacheExpired extends DiaryLoadState {
  final DateTime cachedAt;
  const DiaryCacheExpired({required this.cachedAt});
}

添加这个类之后,项目中有 3 处 switch 表达式会立即报编译错误,因为新状态未被处理。这确保我们不会遗漏任何 UI 分支——而在 Dart 3 之前,这种遗漏只会在运行时以"空白页面"或"无法解释的行为"的形式被发现。


八、实战四:综合重构——DiaryPage 的完整改造

8.1 架构总览

我们将前三个实战整合起来,对 DiaryPage 进行一次完整的架构重构。新的架构图如下:

┌──────────────────────────────────────────────────────────────────────┐
│                    DiaryPage 新架构(Dart 3)                          │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌─────────────────────┐     ┌──────────────────────────────────┐  │
│  │   MoodStorage        │     │  DiaryPage (_DiaryPageState)      │  │
│  │                      │     │                                    │  │
│  │ getWeeklyReport() ───┼────►│  _loadState: DiaryLoadState       │  │
│  │   → Record 返回值     │     │    ├─ DiaryInitial               │  │
│  │                      │     │    ├─ DiaryLoading(progress)      │  │
│  │ insert()             │     │    ├─ DiaryLoaded(entries, ...)   │  │
│  │   → (int, bool)      │     │    └─ DiaryError(message, ...)    │  │
│  │                      │     │                                    │  │
│  │ getAll()             │     │  _saveState: DiarySaveState       │  │
│  │   → List<MoodEntry>  │     │    ├─ SaveIdle                   │  │
│  │                      │     │    ├─ SaveInProgress             │  │
│  └──────────────────────┘     │    ├─ SaveSuccess(savedId)       │  │
│                                │    └─ SaveFailure(message)       │  │
│  ┌──────────────────────┐     │                                    │  │
│  │  Filter Logic         │     │  build() ──► switch(_loadState)  │  │
│  │                      │     │               穷尽性检查           │  │
│  │ moodMatchesFilter()  │     │                                    │  │
│  │ detectMoodShifts()   │     │  ┌──────────────────────────┐     │  │
│  │   → Pattern Matching │     │  │  buildBodyByState()       │     │  │
│  │                      │     │  │  buildSaveIndicator()     │     │  │
│  └──────────────────────┘     │  │  getContextualAdvice()    │     │  │
│                                │  └──────────────────────────┘     │  │
│                                └──────────────────────────────────┘  │
│                                                                      │
│  ┌──────────────────────────────────────────────────────────────┐    │
│  │  数据流向                                                      │    │
│  │                                                                │    │
│  │  MoodStorage.getAll()  →  DiaryLoaded(entries)  →  UI 渲染    │    │
│  │  MoodStorage.insert()  →  SaveSuccess(id)  →  SnackBar + 刷新  │    │
│  │  detectMoodShifts()   →  Record 列表  →  情绪波动提示卡片      │    │
│  └──────────────────────────────────────────────────────────────┘    │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

8.2 核心代码实现

以下是对 _DiaryPageState 的核心部分使用 Dart 3 特性重构的完整代码:

// ── 图例:使用 Dart 3 新特性的部分均以 🆕 标记 ──

class _DiaryPageState extends State<DiaryPage> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  // 🆕 Sealed Class 替代 enum + 多个可空字段
  DiaryLoadState _loadState = const DiaryInitial();
  DiarySaveState _saveState = const SaveIdle();

  // 表单状态
  MoodType? _selectedMood;
  final _noteController = TextEditingController();
  DateTime _selectedDate = DateTime.now();
  int? _editingId;

  
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    widget.moodStorage.addListener(_loadData);
    _loadData();
  }

  void _loadData() {
    setState(() => _loadState = const DiaryLoading(progress: 0.0));

    try {
      // 🆕 Record 解构:一次性获取多个返回值
      final (:allEntries, :weekEntries, :stats) = _fetchAllData();

      setState(() => _loadState = DiaryLoaded(
        allEntries: allEntries,
        weekEntries: weekEntries,
        cacheTimestamp: DateTime.now(),
      ));
    } catch (e) {
      setState(() => _loadState = DiaryError(
        message: e.toString(),
        isRetryable: true,
      ));
    }
  }

  // 🆕 返回命名 Record
  ({List<MoodEntry> allEntries, List<MoodEntry> weekEntries,
    ({double average, MoodType dominant, int total}) stats}) _fetchAllData() {
    final all = widget.moodStorage.getAll();
    final week = widget.moodStorage.getByWeek(DateTime.now());

    final report = widget.moodStorage.getWeeklyReport(DateTime.now());

    return (
      allEntries: all,
      weekEntries: week,
      stats: (average: report.average, dominant: report.dominant, total: report.totalEntries),
    );
  }

  Future<void> _saveMood() async {
    if (_selectedMood == null) return;

    setState(() => _saveState = const SaveInProgress());

    try {
      final now = DateTime.now();
      final timestamp = DateTime(
        _selectedDate.year, _selectedDate.month, _selectedDate.day,
        now.hour, now.minute, now.second,
      );

      if (_editingId != null) {
        // ... 更新逻辑
        await widget.moodStorage.update(_editingId!, MoodEntry(
          moodType: _selectedMood!,
          note: _noteController.text.isEmpty ? null : _noteController.text,
          createdAt: timestamp,
          updatedAt: timestamp,
        ));
        setState(() => _saveState = SaveSuccess(savedId: _editingId!));
      } else {
        // 🆕 insert 返回 (id, isNewRecord)
        final (id, _) = await widget.moodStorage.insert(MoodEntry(
          moodType: _selectedMood!,
          note: _noteController.text.isEmpty ? null : _noteController.text,
          createdAt: timestamp,
          updatedAt: timestamp,
        ));
        setState(() => _saveState = SaveSuccess(savedId: id));
      }

      _noteController.clear();
      _editingId = null;
      setState(() => _selectedMood = null);

      // 延迟重置保存状态
      Future.delayed(const Duration(seconds: 1), () {
        if (mounted) setState(() => _saveState = const SaveIdle());
      });

    } catch (e) {
      setState(() => _saveState = SaveFailure(message: e.toString()));
    }
  }

  // 🆕 switch expression 生成问候语
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('情绪日记'),
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: '记录'),
            Tab(text: '时间线'),
            Tab(text: '统计'),
          ],
        ),
      ),
      // 🆕 根据状态渲染主体内容
      body: switch (_loadState) {
        DiaryInitial()  => const SizedBox.shrink(),
        DiaryLoading()  => const Center(child: CircularProgressIndicator()),
        DiaryLoaded(:final allEntries, :final weekEntries) =>
          _buildTabContent(allEntries, weekEntries),
        DiaryError(:final message, :final isRetryable) =>
          _buildErrorView(message, isRetryable),
      },
    );
  }

  // 🆕 switch expression 渲染 Tab 内容
  Widget _buildTabContent(List<MoodEntry> all, List<MoodEntry> week) {
    return TabBarView(
      controller: _tabController,
      children: [
        _buildRecordTab(),
        _buildTimelineTab(all),
        _buildStatsTab(week),
      ],
    );
  }

  Widget _buildStatsTab(List<MoodEntry> weekEntries) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('本周心情', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
          const SizedBox(height: 16),
          MoodChart(weekMoods: weekEntries),
          const SizedBox(height: 16),
          // 🆕 使用 Records + Pattern 生成更丰富的摘要
          Text(_generateWeekSummary(weekEntries), style: AppTheme.greetingStyle),
          if (weekEntries.length >= 2) ...[
            const SizedBox(height: 12),
            _buildMoodShiftCard(weekEntries),
          ],
        ],
      ),
    );
  }

  // 🆕 综合使用 Records + Pattern Matching 生成周报摘要
  String _generateWeekSummary(List<MoodEntry> entries) {
    if (entries.isEmpty) return '本周还没有记录哦,来记录第一份心情吧 🌱';

    final avg = entries.map((m) => m.moodType.value).reduce((a, b) => a + b) / entries.length;

    // 🆕 根据心情类型分布给出针对性建议
    final moodCounts = <MoodType, int>{};
    for (final e in entries) {
      moodCounts[e.moodType] = (moodCounts[e.moodType] ?? 0) + 1;
    }
    final dominant = moodCounts.entries
        .fold<MapEntry<MoodType, int>?>(
            null, (max, e) => max == null || e.value > max.value ? e : max)
        ?.key;

    // 🆕 switch expression:多维度建议生成
    return switch ((avg, dominant, entries.length)) {
      (>= 4.0, MoodType.happy, >= 3) =>
        '这周充满正能量!${entries.length}次记录中大部分都是好心情 🌞',
      (>= 4.0, _, _) =>
        '这周心情很阳光!继续保持 🌞',
      (>= 3.0, MoodType.calm, >= 3) =>
        '这周心态平和,'平静'是最常出现的状态——稳定的力量 🍃',
      (>= 3.0, _, _) =>
        '这周心情平稳,一切刚刚好 🍃',
      (>= 2.0, MoodType.tired, _) =>
        '这周看起来有些疲惫,记得给自己足够的休息时间 💆',
      (>= 2.0, _, _) =>
        '这周有些低落,给自己一个拥抱 🫂',
      (_, MoodType.angry, _) =>
        '愤怒是正常的情绪,试着用呼吸练习来平复 🫁',
      _ => '这周辛苦了,好好休息一下吧 💤',
    };
  }

  // 🆕 情绪波动检测与展示
  Widget _buildMoodShiftCard(List<MoodEntry> weekEntries) {
    final shifts = detectMoodShifts(weekEntries);
    if (shifts.isEmpty) return const SizedBox.shrink();

    return Card(
      color: Colors.orange.shade50,
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('🔍 情绪波动检测',
                style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
            const SizedBox(height: 8),
            ...shifts.map((shift) => Text(
              '${shift.before.createdAt.month}/${shift.before.createdAt.day} '
              '${shift.before.moodType.emoji}${shift.after.moodType.emoji} '
              '(${shift.delta > 0 ? "上升" : "下降"} ${shift.delta.abs()} 档)',
              style: const TextStyle(fontSize: 13, color: Colors.brown),
            )),
          ],
        ),
      ),
    );
  }

  Widget _buildErrorView(String message, bool isRetryable) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(Icons.error_outline, size: 64, color: Colors.red.shade300),
          const SizedBox(height: 16),
          Text('加载失败', style: TextStyle(fontSize: 18, color: Colors.red.shade400)),
          const SizedBox(height: 8),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 32),
            child: Text(message, textAlign: TextAlign.center,
                style: const TextStyle(color: Colors.grey)),
          ),
          if (isRetryable) ...[
            const SizedBox(height: 16),
            ElevatedButton.icon(
              onPressed: _loadData,
              icon: const Icon(Icons.refresh),
              label: const Text('重新加载'),
            ),
          ],
        ],
      ),
    );
  }

  // ── 以下为未改动的方法,保留原有实现 ──
  Widget _buildRecordTab() { /* ... 见原实现 ... */ }
  Widget _buildTimelineTab(List<MoodEntry> all) { /* ... 见原实现 ... */ }
  void _deleteMood(int id) { /* ... 见原实现 ... */ }
  void _showMoodDetail(MoodEntry mood) { /* ... 见原实现 ... */ }

  
  void dispose() {
    widget.moodStorage.removeListener(_loadData);
    _tabController.dispose();
    _noteController.dispose();
    super.dispose();
  }
}

8.3 关键改进点总结

  1. 状态管理:从 enum + 可空字段 变为 Sealed Class,消除了非法状态。
  2. 数据传递_fetchAllData() 使用命名 Record 一次性返回三个数据集。
  3. UI 渲染build()_generateWeekSummary() 使用 switch expression,编译器保证穷尽性。
  4. 业务逻辑detectMoodShifts() 使用 Pattern Matching 实现分级条件判断。
  5. 保存反馈insert() 返回 (int id, bool isNewRecord),提供更多上下文信息。

九、代码量对比与收益分析

我们将 E-Brufen 项目中使用 Dart 3 新特性重构的模块进行了统计。以下数据基于实际重构结果:

9.1 行数统计

模块重构前(行)重构后(行)减少减少比例
MoodStorage 返回值12(临时类) + 8(调用处) = 206-14-70%
情绪过滤逻辑187-11-61%
状态枚举(DiaryPage)34(enum + 可空字段)35(Sealed Class 定义)+1持平*
状态消费代码4528-17-38%
周报摘要生成2214-8-36%
问候语生成(HomePage)127-5-42%
_generateWeekSummary()1512-3-20%
合计166109-57约 -34%

*注:Sealed Class 定义本身比 enum + 字段略长,但消费端代码大幅减少。总体收益在消费端。

9.2 质量指标对比

指标重构前重构后改善
可空类型字段数5(entries?, errorMessage?, 等)0消除所有不必要的可空
强制类型转换(as3 处0 处完全消除
未处理的错误状态2 种(运行时才能发现)0 种(编译期报错)风险清零
临时数据类1 个(DailyMoodResult0(Record 替代)零冗余
switch 穷尽性无(手动维护)编译器保证自动保障
逻辑分支总数137减少 46%

9.3 最值得使用 Dart 3 特性的 Top 5 场景

排名场景推荐特性收益
1异步状态管理Sealed Class消除非法状态,穷尽性检查
2函数多值返回Record零依赖,语义清晰
3多层条件判断Switch Expression代码量减少 50%+
4JSON/Map 解构Pattern Destructure消除类型转换
5API 响应建模Sealed Class自动处理所有响应类型

十、鸿蒙平台兼容性说明

10.1 Dart SDK 版本要求

Dart 3 的 Records、Patterns 和 Sealed Classes 需要 Dart SDK >= 3.0.0。对于鸿蒙(HarmonyOS)平台的 Flutter 项目,需要注意以下几点:

项目要求说明
Dart SDK>= 3.0.0建议使用 3.2+ 以获得完整特性支持
Flutter SDK>= 3.10.0包含 Dart 3 的 Flutter 版本
鸿蒙 Flutter Engine需确认引擎支持的 Dart 版本AtomGit 上的鸿蒙 Flutter 引擎需同步更新
DevEco Studio>= 5.0对最新 Flutter 支持更好

10.2 鸿蒙平台的已验证特性

我们在 E-Brufen 项目中已验证以下特性在鸿蒙设备(Mate 60 Pro,HarmonyOS NEXT)上正常运行:

特性验证状态备注
Record 类型✅ 正常位置 Record 和命名 Record 均正常工作
Switch Expression✅ 正常包括 when 条件守卫
Sealed Class✅ 正常穷尽性检查有效
Object Pattern✅ 正常对象解构正常
List Pattern✅ 正常包括 ... rest pattern
Map Pattern✅ 正常key-value 解构正常
Logical-or Pattern✅ 正常case A || B 正常工作
Relational Pattern✅ 正常case >= 3 正常工作

10.3 pubspec.yaml 配置

# pubspec.yaml
environment:
  sdk: '>=3.2.0 <4.0.0'
  # 鸿蒙项目通常使用此范围

dependencies:
  flutter:
    sdk: flutter
  # 确保其他依赖也兼容 Dart 3
  hive_ce: ^2.0.0
  # ...

十一、迁移策略与注意事项

11.1 渐进式迁移路线图

我们推荐分四个阶段逐步引入 Dart 3 特性,避免一次性大规模重构带来的风险:

┌─────────────────────────────────────────────────────────────────┐
│               Dart 3 特性迁移路线图                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  阶段 1:Switch Expression(1-2 天,低风险)                      │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  • 将简单 if-else 链替换为 switch expression          │       │
│  │  • 添加 exhaustiveness 检查(添加 default 分支)      │       │
│  │  • 影响范围:工具函数、映射函数                          │       │
│  │  • 改动文件示例:mood_chart.dart (summaryText)         │       │
│  └──────────────────────────────────────────────────────┘       │
│                          ▼                                       │
│  阶段 2:Records(1-2 天,低风险)                                │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  • 将"只有一次使用"的临时类替换为 Record                │       │
│  │  • 将 Map<String, dynamic> 返回值替换为命名 Record       │       │
│  │  • 影响范围:数据层方法返回值                              │       │
│  │  • 改动文件示例:mood_storage.dart (getWeeklyReport)   │       │
│  └──────────────────────────────────────────────────────┘       │
│                          ▼                                       │
│  阶段 3:Pattern Destructuring(1-2 天,低风险)                  │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  • 在变量声明中使用 List/Map/Object 解构               │       │
│  │  • 使用 if-case 替代类型检查 + 转换                     │       │
│  │  • 影响范围:JSON 解析、数据转换                         │       │
│  └──────────────────────────────────────────────────────┘       │
│                          ▼                                       │
│  阶段 4:Sealed Classes(2-3 天,中风险)                        │
│  ┌──────────────────────────────────────────────────────┐       │
│  │  • 为每个有状态组件创建 Sealed Class 状态模型           │       │
│  │  • 替换 enum + 可空字段                                │       │
│  │  • 在 build() 中使用 switch expression 消费状态        │       │
│  │  • 影响范围:所有 StatefulWidget                        │       │
│  │  • 改动文件示例:diary_page.dart, soundscape_page.dart  │       │
│  └──────────────────────────────────────────────────────┘       │
│                                                                 │
│  总预计工时:5-9 天                                               │
│  建议:每次迁移后运行完整测试套件,确保没有回归                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

11.2 注意事项

  1. Record 不替代所有临时类:如果数据需要 JSON 序列化、跨文件共享、或添加自定义方法,仍然使用 class。
  2. Sealed Class 的子类必须在同一个文件中:如果状态需要跨模块引用,考虑使用 sealed + 工厂构造函数的模式,或将状态定义放在共享库中。
  3. Switch 穷尽性检查需要 dart analyze:IDE 内的即时提示有时不够准确,建议在 CI 中运行 dart analyze 来捕获遗漏。
  4. 时间复杂度和性能:Record 的解构和 switch expression 的匹配都是 O(1) 操作,不会引入性能损耗。
  5. 避免过度使用:不要把每个 if-else 都改成 switch expression。当分支数少于 3 个时,传统 if-else 可能更可读。

11.3 团队协作建议

当团队中有开发者还不太熟悉 Dart 3 特性时:

  • 代码审查清单:在 PR 模板中添加 Dart 3 特性的检查项。
  • 团队分享会:花 30 分钟演示一次典型的 Sealed Class 重构过程。
  • 渐进式采纳:不强推,让成员从 Switch Expression 开始逐步尝试。

十二、总结

Dart 3 的三大新特性——Records、Patterns、Sealed Classes——不是孤立存在的语法糖,而是相互配合、形成闭环的语言能力升级。在 E-Brufen 项目的实践中,我们的体会可以归纳为以下五点:

1. Records 让"临时数据"有了正式身份。 函数返回多值不再需要写一个只用一次的类,命名 Record 的类型提示比 Map 清晰得多。对于需要零依赖、零开销的数据传递场景,Record 是最佳选择。

2. Patterns 改变了我们对条件逻辑的思考方式。 Switch Expression 配合 when 守卫和逻辑或模式,将原本分散在多个 if-else 分支中的逻辑收敛到一个表达式中。它不仅减少了代码行数(平均 40%+),更重要的是让逻辑路径一目了然。

3. Sealed Class 实现了编译期的状态安全保障。 这是三者中收益最深远的特性。当你新增一个状态类型,所有遗漏处理的 switch 表达式都会立即报错——这种"编译器帮你找 bug"的体验,让运行时崩溃成为过去式。

4. 三者的组合使用产生 1+1+1 > 3 的效果。 Sealed Class 定义的 ADT + Switch Expression 的穷尽性检查 + Record 的轻量数据传递,构成了一套完整的"类型驱动开发"范式。

5. 鸿蒙平台完全兼容,无额外适配成本。 只要是 Dart SDK >= 3.0.0 的项目,无论是 Android、iOS 还是 HarmonyOS,这些特性的行为完全一致。

正如 Herb Sutter(C++ 标准委员会主席)所说:"好的抽象应该让非法状态无法表达。"Dart 3 正是朝这个方向迈出的重要一步。如果你的项目已经使用 Dart 3 或计划升级,我们强烈建议你尝试将这些新特性引入日常开发中——从一个小模块开始,感受类型安全带来的心智减负。


作者简介

E-Brufen Dev,全栈开发者,专注于 Flutter 跨平台与鸿蒙(HarmonyOS)生态。维护 AtomGit 上的 Flutter 鸿蒙客户端 开源项目,撰写《鸿蒙 Flutter 实战》系列文章,分享真实的适配经验与技术思考。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐