【Flutter for OpenHarmony】第三方库 Equatable 数据模型的鸿蒙化适配与实战指南!!!
【Flutter for OpenHarmony】Equatable 数据模型的鸿蒙化适配与实战指南!!!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
前言大大
大家好,我是 IntMainJHy。
上一篇文章讲了 Provider 状态管理,有个细节没展开——我用的 MoodRecord、MedicineRecord、PeriodRecord 这些数据模型都继承了 Equatable。
为什么用 Equatable?这个问题我问过 Claude,它给了一堆高大上的回答,但我当时完全听不懂。直到我在鸿蒙上跑代码,被一个奇怪的 bug 卡了三天,才彻底理解 Equatable 的价值。
—d
一、先说我的踩坑故事
那个让我崩溃的 bug!!!!!
现象是这样的:我写了一个「判断两个情绪记录是否是同一天」的函数:
bool isSameDay(MoodRecord a, MoodRecord b) {
return a.date.year == b.date.year &&
a.date.month == b.date.month &&
a.date.day == b.date.day;
}
后来改成这样:
bool isSameDay(MoodRecord a, MoodRecord b) {
return a == b; // 用了 == 比较
}
Android 上跑得好好的,鸿蒙上疯狂报错。有时候 a == b 返回 true,有时候返回 false,玄学得很。
后来才发现原因:Flutter 默认的 == 比较的是对象引用,不是内容!
void main() {
final record1 = MoodRecord(
id: '1',
mood: MoodType.happy,
date: DateTime(2026, 5, 1),
);
final record2 = MoodRecord(
id: '1', // 同一个 id,同一个时间
mood: MoodType.happy,
date: DateTime(2026, 5, 1),
);
print(record1 == record2); // false!内容一样但不是同一个对象
// Flutter 默认的 == 比较的是内存地址
}
解决方案:让模型继承 Equatable,它会帮你实现基于内容的 == 比较。
二、Equatable 是什么?
Equatable 是 Flutter 社区最常用的「值相等性」解决方案。简单说:
- 普通 Dart 类:
==比较对象引用(两个不同对象,内容一样,也是 false) - Equatable:
==比较内容(两个内容一样的对象,视为相等)
三、依赖引入
# pubspec.yaml
dependencies:
equatable: ^2.0.5
✅ 好消息:Equatable 是纯 Dart 包,不需要任何平台适配!可以直接在鸿蒙上使用。
四、三大模块的 Equatable 改造
4.1 情绪记录模型
// lib/models/health/mood_model.dart
import 'package:equatable/equatable.dart';
// ==================== 情绪类型 ====================
enum MoodType {
happy(emoji: '😊', label: '开心', value: 9, color: 0xFF4CAF50),
excited(emoji: '🤩', label: '兴奋', value: 10, color: 0xFFFF9800),
calm(emoji: '😌', label: '平静', value: 7, color: 0xFF2196F3),
anxious(emoji: '😰', label: '焦虑', value: 4, color: 0xFFFF5722),
sad(emoji: '😢', label: '难过', value: 3, color: 0xFF9C27B0),
angry(emoji: '😠', label: '生气', value: 2, color: 0xFFF44336);
final String emoji;
final String label;
final int value;
final int color;
const MoodType({
required this.emoji,
required this.label,
required this.value,
required this.color,
});
}
// ==================== 情绪记录 ====================
class MoodRecord extends Equatable {
final String id;
final MoodType mood;
final int energyLevel;
final int stressLevel;
final List<String>? triggers;
final String? note;
final DateTime date;
final DateTime createdAt;
const MoodRecord({
required this.id,
required this.mood,
required this.energyLevel,
required this.stressLevel,
this.triggers,
this.note,
required this.date,
required this.createdAt,
});
String get formattedTime {
return '${createdAt.hour.toString().padLeft(2, '0')}:${createdAt.minute.toString().padLeft(2, '0')}';
}
// Equatable 要求:指定哪些字段参与相等性比较
List<Object?> get props => [id, mood, energyLevel, stressLevel, triggers, note, date];
}
// ==================== 触发因素 ====================
class MoodTriggers {
static const options = [
'工作', '学习', '家庭', '感情', '健康', '睡眠', '社交', '天气',
];
}
4.2 药物记录模型
// lib/models/health/medicine_model.dart
import 'package:equatable/equatable.dart';
// ==================== 药物状态枚举 ====================
enum MedicineStatus {
pending(label: '待服用', color: 0xFFFF9800),
taken(label: '已服用', color: 0xFF4CAF50),
skipped(label: '已跳过', color: 0xFF9E9E9E);
final String label;
final int color;
const MedicineStatus({required this.label, required this.color});
}
// ==================== 药物信息 ====================
class Medicine extends Equatable {
final String id;
final String name;
final String dosage; // 剂量,如 "1片"
final String unit; // 单位,如 "mg"
final String? instructions; // 用药说明
final String? icon; // 图标 emoji
final int? color; // 背景色
const Medicine({
required this.id,
required this.name,
required this.dosage,
required this.unit,
this.instructions,
this.icon,
this.color,
});
List<Object?> get props => [id, name, dosage, unit, instructions, icon, color];
}
// ==================== 服药记录 ====================
class MedicineRecord extends Equatable {
final String id;
final String medicineId;
final String medicineName;
final String dosage;
final String unit;
final DateTime scheduledTime;
final DateTime? takenTime;
final MedicineStatus status;
const MedicineRecord({
required this.id,
required this.medicineId,
required this.medicineName,
required this.dosage,
required this.unit,
required this.scheduledTime,
this.takenTime,
required this.status,
});
String get formattedScheduledTime {
return '${scheduledTime.hour.toString().padLeft(2, '0')}:${scheduledTime.minute.toString().padLeft(2, '0')}';
}
List<Object?> get props => [id, medicineId, scheduledTime, takenTime, status];
}
4.3 生理期记录模型!!!!
// lib/models/health/period_model.dart
import 'package:equatable/equatable.dart';
// ==================== 经量等级 ====================
enum PeriodFlowLevel {
light(label: '少量', color: 0xFFFCE4EC),
medium(label: '中等', color: 0xFFF8BBD0),
heavy(label: '大量', color: 0xFFF48FB1),
veryHeavy(label: '非常多', color: 0xFFE91E63);
final String label;
final int color;
const PeriodFlowLevel({required this.label, required this.color});
}
// ==================== 生理期设置 ====================
class PeriodSettings extends Equatable {
final int cycleLength; // 周期长度,默认 28 天
final int periodLength; // 经期长度,默认 5 天
final DateTime? lastPeriodStart; // 上次经期开始日期
const PeriodSettings({
this.cycleLength = 28,
this.periodLength = 5,
this.lastPeriodStart,
});
PeriodSettings copyWith({
int? cycleLength,
int? periodLength,
DateTime? lastPeriodStart,
}) {
return PeriodSettings(
cycleLength: cycleLength ?? this.cycleLength,
periodLength: periodLength ?? this.periodLength,
lastPeriodStart: lastPeriodStart ?? this.lastPeriodStart,
);
}
List<Object?> get props => [cycleLength, periodLength, lastPeriodStart];
}
// ==================== 生理期记录 ====================
class PeriodRecord extends Equatable {
final String id;
final DateTime startDate;
final DateTime? endDate;
final int duration; // 持续天数
final PeriodFlowLevel flowLevel; // 经量等级
final List<String>? symptoms; // 症状列表
const PeriodRecord({
required this.id,
required this.startDate,
this.endDate,
required this.duration,
required this.flowLevel,
this.symptoms,
});
List<Object?> get props => [id, startDate, endDate, duration, flowLevel, symptoms];
}
// ==================== 周期预测 ====================
class PeriodPrediction extends Equatable {
final DateTime predictedDate;
final int daysUntil;
final String phase; // 当前阶段:卵泡期、排卵期、黄体期、经期
const PeriodPrediction({
required this.predictedDate,
required this.daysUntil,
required this.phase,
});
List<Object?> get props => [predictedDate, daysUntil, phase];
}
// ==================== 症状选项 ====================
class PeriodSymptoms {
static const options = [
'腹痛', '腰痛', '头痛', '乳房胀痛', '腹胀',
'情绪波动', '疲劳', '失眠', '食欲改变', '痤疮',
];
}
五、Equatable 的实用技巧
5.1 在 Provider 中判断记录是否存在
class HealthProvider extends ChangeNotifier {
final List<MoodRecord> _moodRecords = [];
final List<PeriodRecord> _periodRecords = [];
final List<MedicineRecord> _medicineRecords = [];
// 判断今天是否已记录情绪
bool hasTodayMood() {
final today = DateTime.now();
// 使用 Equatable 的 == 比较
return _moodRecords.any((record) =>
record.date.year == today.year &&
record.date.month == today.month &&
record.date.day == today.day
);
}
// 获取今天的情绪记录
MoodRecord? getTodayMood() {
final today = DateTime.now();
try {
return _moodRecords.firstWhere((record) =>
record.date.year == today.year &&
record.date.month == today.month &&
record.date.day == today.day
);
} catch (e) {
return null;
}
}
}
5.2 在 UI 中比较状态变化
class MoodTrendChart extends StatelessWidget {
final MoodRecord? previousRecord;
final MoodRecord? currentRecord;
const MoodTrendChart({
this.previousRecord,
this.currentRecord,
});
Widget build(BuildContext context) {
// 使用 Equatable 的 == 判断是否需要更新图表
if (currentRecord == null) {
return const Center(child: Text('暂无数据'));
}
// 当记录变化时,只更新图表部分
return Column(
children: [
if (previousRecord != null && previousRecord != currentRecord)
_MoodChangeIndicator(
from: previousRecord!,
to: currentRecord!,
),
_Chart(data: currentRecord!),
],
);
}
}
5.3 周期算法中的日期比较
class PeriodCalculator {
// 判断是否在经期
static bool isInPeriod(DateTime date, PeriodSettings settings) {
if (settings.lastPeriodStart == null) return false;
final daysSinceStart = date.difference(settings.lastPeriodStart!).inDays;
return daysSinceStart >= 0 && daysSinceStart < settings.periodLength;
}
// 获取周期中的第几天
static int? getCycleDay(DateTime date, PeriodSettings settings) {
if (settings.lastPeriodStart == null) return null;
final daysSinceStart = date.difference(settings.lastPeriodStart!).inDays;
if (daysSinceStart < 0) return null;
return daysSinceStart % settings.cycleLength + 1;
}
// 预测下次经期
static PeriodPrediction? getNextPeriod(DateTime date, PeriodSettings settings) {
if (settings.lastPeriodStart == null) return null;
final lastStart = settings.lastPeriodStart!;
final cycleDay = date.difference(lastStart).inDays % settings.cycleLength;
final daysUntilNext = settings.cycleLength - cycleDay;
return PeriodPrediction(
predictedDate: date.add(Duration(days: daysUntilNext)),
daysUntil: daysUntilNext,
phase: _getPhase(cycleDay, settings.periodLength),
);
}
static String _getPhase(int cycleDay, int periodLength) {
if (cycleDay < periodLength) return '经期';
if (cycleDay < periodLength + 5) return '卵泡期';
if (cycleDay < periodLength + 9) return '排卵期';
return '黄体期';
}
}
六、鸿蒙平台踩坑实录!!!!
🕳️ 坑 1:DateTime 的时区问题导致 Equatable 失效
报错信息:
flutter: Expected: '2026-05-01 00:00:00.000'
flutter: Actual: '2026-05-01 08:00:00.000'
问题场景:
我保存了一个 DateTime,然后从数据库读取出来,两个 DateTime 明明应该是同一个时间,但 == 比较返回 false!
// 保存时
final record = MoodRecord(
id: '1',
mood: MoodType.happy,
date: DateTime.now(), // 2026-05-01 00:00:00 北京时间
);
// 读取时
final saved = await storage.getRecord('1');
print(record.date == saved.date); // false!
// 因为 DateTime 存储时可能转成 UTC,读取时又转回本地
解决步骤:
// 方案 1:使用 DateTime.utc() 确保时区一致
class MoodRecord extends Equatable {
// ...
final DateTime date;
MoodRecord({
// ...
DateTime? date,
}) : date = date ?? DateTime.now();
// 在存储时转换
Map<String, dynamic> toJson() {
return {
'date': date.toUtc().toIso8601String(), // 转成 UTC 存储
};
}
factory MoodRecord.fromJson(Map<String, dynamic> json) {
return MoodRecord(
// 从 UTC 读取
date: DateTime.parse(json['date'] as String).toLocal(),
);
}
}
// 方案 2:只比较日期部分,忽略时间
List<Object?> get props => [
id, mood, energyLevel, stressLevel, triggers, note,
DateTime(date.year, date.month, date.day), // 只保留日期
];
🕳️ 坑 2:继承多个 Equatable 子类时的比较问题
报错信息:
Unbalanced: [TickerProviderStateMixin] vs [Equatable]
问题场景:
我想让 Provider 也继承 Equatable,方便测试时比较状态。
// ❌ 错误:Provider 继承了 TickerProviderStateMixin 和 Equatable
// 会导致状态管理冲突
class MoodProvider extends ChangeNotifier with TickerProviderStateMixin, Equatable {
// ...
}
// ✅ 正确:Provider 用 ChangeNotifier 就够了
// Equatable 主要用于数据模型
class MoodProvider extends ChangeNotifier {
// Provider 内部使用 MoodRecord 等 Equatable 模型
MoodRecord? _currentMood;
void updateMood(MoodRecord newMood) {
// 直接比较 MoodRecord
if (_currentMood != newMood) { // 现在能正确比较了
_currentMood = newMood;
notifyListeners();
}
}
}
🕳️ 坑 3:List 字段的 props 比较陷阱
报错信息:
flutter: Expected: [工作, 学习]
flutter: Actual: [工作, 学习]
flutter: But: <[工作, 学习]>
问题场景:
我的 MoodRecord 有一个 triggers: List<String>? 字段。创建两个完全相同的记录,但 == 返回 false!
// 问题根源:List 的 == 比较的是引用,不是内容
final list1 = ['工作', '学习'];
final list2 = ['工作', '学习'];
print(list1 == list2); // false!两个不同的 List 对象
// Equatable 的 props 检查会失败
final record1 = MoodRecord(
triggers: ['工作', '学习'],
);
final record2 = MoodRecord(
triggers: ['工作', '学习'],
);
print(record1 == record2); // false!
解决步骤:
// 方案 1:使用 ListEquality(来自 collection 包)
import 'package:equatable/equatable.dart';
import 'package:collection/collection.dart';
class MoodRecord extends Equatable {
final List<String>? triggers;
List<Object?> get props => [triggers]; // Equatable 会用 == 比较列表
// 实际测试发现:Equatable 确实能比较内容!
// 但需要确保是同一个类型的 List
}
// 方案 2:使用 unmodifiable list
class MoodRecord extends Equatable {
final List<String>? triggers;
MoodRecord({this.triggers});
// 存储时转换为不可变列表
factory MoodRecord.create({List<String>? triggers}) {
return MoodRecord(
triggers: triggers != null ? List.unmodifiable(triggers) : null,
);
}
List<Object?> get props => [triggers];
}
// 方案 3:比较列表长度和元素
bool isEquivalent(MoodRecord other) {
if (triggers == null && other.triggers == null) return true;
if (triggers == null || other.triggers == null) return false;
return triggers!.length == other.triggers!.length &&
triggers!.every((t) => other.triggers!.contains(t));
}
七、功能验证清单
- 创建两个内容相同的记录,
==返回 true - 从数据库读取后,模型比较仍然正确
- 列表筛选(
.where,.firstWhere)能正确匹配记录 - Provider 的状态比较能正确触发更新
- 周期预测计算结果准确


八、大一学生心得总结
说实话,学 Equatable 之前我写的代码经常出问题。比如判断「今天是否记录过情绪」,我用的是:
final todayRecords = records.where((r) =>
r.date.year == now.year && r.date.month == now.month && r.date.day == now.day
);
if (todayRecords.isNotEmpty) { ... }
用了 Equatable 后可以直接:
final todayRecord = records.cast<MoodRecord?>().firstWhere(
(r) => r?.date.year == now.year && r?.date.month == now.month && r?.date.day == now.day,
orElse: () => null,
);
if (todayRecord != null) { ... }
代码更优雅,而且不会因为对象引用不同而出现奇怪的 bug。
关于鸿蒙适配的体会:
Equatable 本身不需要适配,但我遇到的两个坑(时区问题、List 比较)其实在任何平台都会遇到,只是在鸿蒙上更容易触发。因为鸿蒙的时区处理和 Android 略有差异,所以 DateTime 相关的比较要格外小心。
建议:如果你写的 App 涉及日期时间的比较,一定要:
- 统一使用 UTC 或本地时间,不要混用
- 存储时用
toUtc(),读取时用toLocal() - 如果只需要日期比较,丢弃时间部分
希望这篇文章对你有帮助!有问题欢迎留言~
作者:IntMainJHy
身份:上海本科大一计算机专业学生
博客:CSDN @IntMainJHy
项目:my_ohos_app (Flutter + OpenHarmony 健康追踪应用)
首发于 CSDN Flutter for OpenHarmony 专题
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)