先说结论:Flutter 写鸿蒙应用,真的能跑起来,而且待办事项这种偏工具型的应用,完全可以用一套代码同时覆盖 Android、iOS 和鸿蒙。最近我刚好把一个待办事项项目从 Android 扩展到鸿蒙,中间就卡在“优先级排序”这块功能上,排序做完之后,鸿蒙适配又折腾了一周多。这篇教程我按自己真实项目的流程来写,从业务设计、排序算法、排序状态管理,到 Flutter 在鸿蒙上的环境搭建、真机调试、常见坑位,一次讲透。
功利地说,这个项目适合两类人看:一是刚接触鸿蒙开发、想知道 Flutter 能不能直接上鸿蒙的朋友,二是已经在做跨平台、想把“待办事项优先级排序”这种看似简单但容易做歪的功能做扎实的开发者。我会把产品设计思路、排序规则模型、Dart 代码实现、鸿蒙编译注意事项都拆开讲,你照着改,不一定要用我的代码结构,但排序规则和适配思路可以直接抄。
1. 项目设计拆解:先定边界,再定排序规则
1.1 技术选型:Flutter 上鸿蒙的实际可行路径
鸿蒙应用开发现在有几种路线,我先说结论:如果你的项目里已经有一套 Flutter 代码,完全没必要为了鸿蒙再单开一套 ArkTS 原生。
目前常见的三条路线是这样的:
- ArkTS 原生开发:用 DevEco Studio 写鸿蒙原生应用,生态集成最直接,调 HarmonyOS 的系统能力最爽,但代价是代码完全无法在 Android/iOS 复用。对中小团队来说,维护三套客户端代码的成本非常现实。
- uni-app 等跨端框架:上手快,H5 那套能直接打包,但遇到复杂列表、拖拽排序、高性能滚动这类需求时,WebView 渲染的流畅度会打折扣。待办这种高频交互应用,滚动卡一下用户立刻能感觉到。
- Flutter 跨平台方案:Flutter 官方并不直接支持鸿蒙,但 OpenHarmony SIG(特别兴趣小组)长期维护了一个适配分支
flutter_flutter,把 Flutter 引擎跑在了 OpenHarmony 及基于 OpenHarmony 的系统上。这套方案的好处是 Dart 层代码完全不用动,UI、状态管理、排序逻辑都能复用,代价是部分原生插件需要找鸿蒙适配版本。
我最终选的是 Flutter。选它的核心原因很朴素:业务逻辑和界面层只需要写一份,尤其是“待办事项优先级排序”这种纯逻辑功能,在 Dart 层写好之后,Android、iOS、鸿蒙三端表现完全一致,不用各写一遍排序。flutter_flutter 分支目前在社区里迭代很活跃,实测跑待办这种轻量应用是稳的,但你要有心理准备:它毕竟不是官方主分支,版本跟不上、插件缺失这些事后面一定会遇到。
1.2 需求拆解:优先级排序的规则远不止三档
“待办事项优先级排序”听起来简单,不就是高、中、低三档排一下吗?实际做产品设计的时候你会发现,用户眼里的“合理排序”远比这复杂。
我整理这个项目时,把需求拆成了四个维度:
| 排序维度 | 规则 | 设计原因 |
|---|---|---|
| 完成状态 | 未完成任务优先,已完成任务沉底 | 用户打开列表的第一诉求是处理未完成的事 |
| 优先级 | 高(权重 2)> 中(权重 1)> 低(权重 0) | 核心排序依据,采用数值权重便于后续扩展更多档位 |
| 截止日期 | 截止日期越近越靠前,无截止日期排最后 | 同优先级下,时间压力越大越需要优先处理 |
| 创建时间 | 创建时间越新越靠前 | 同优先级同截止日期时,后创建的任务默认视为更新、更重要 |
这里最关键的设计决策是:排序维度要有层次,不能简单地把优先级一维排完。如果只按高中低排,那同一优先级里的几十个任务谁先谁后就只能看运气了。用户看到两个同为“高优先级”的任务排在一起,自然会对先后顺序产生疑问——“为什么 A 在 B 前面?”这个问题的答案必须能解释得清。
我最终采用的规则链是:未完成 > 优先级权重 > 截止日期 > 创建时间。这套规则的优点是可解释性强,任何时候用户问起排序依据,产品经理都能给出明确回答。你如果后面要加“标签筛选”“置顶任务”这类功能,在这个规则链上追加维度就行,不需要重构。
1.3 技术栈规划:状态管理与本地存储
技术栈方面,这个项目的核心诉求是:排序状态可预测、数据可持久化、鸿蒙平台上插件能跑。
状态管理我选了 Bloc(Cubit 模式),不是因为它最流行,而是因为它天然适合这种场景。待办列表的每次变化(新增、删除、切换完成状态、切换排序模式)都是一个明确的事件,Cubit 接收到事件后,在内部统一完成“重新排序 + 重新持久化”两个操作,然后 emit 一个新状态,UI 层只负责根据状态渲染。排序逻辑被收敛到一个地方,不会出现“我明明改了列表,但 UI 没刷新”这种问题。
本地存储我没有引入数据库,用的 shared_preferences 存 JSON。判断依据很简单:待办事项的数据量有限,单用户单日新增也就几十条,JSON 序列化完全够用。而且 shared_preferences 在鸿蒙上有适配包,接入成本最低。如果你的待办项目后面要做多用户、云端同步或者复杂查询,再换成 sqflite_ohos(鸿蒙上的 SQLite 适配库)也不迟,业务层做一层仓储抽象就能平滑切换。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能实现:待办事项优先级排序的代码实战
2.1 数据模型:从枚举到 Task 类
排序的核心是数据模型。我先定义一个“优先级”枚举,这里要注意一个小技巧:给枚举增加一个数值字段 priorityScore,而不是靠枚举名称去比较大小。
dart复制enum TaskPriority {
low(0),
medium(1),
high(2);
const TaskPriority(this.priorityScore);
/// 高权重排前面,所以排序时直接比较数值即可
final int priorityScore;
}
为什么不用 int 裸类型直接存优先级?因为枚举的可读性更强,UI 层可以直接根据枚举判断颜色和文案,而且 Dart 枚举是编译期校验的,传错值编译器就会报错。后续想加“紧急”档位,只需要加一个枚举值和权重 3,排序函数完全不用改。
接下来是 Task 模型。它承载了排序所需的全部字段:
dart复制class Task {
final String id;
final String title;
final bool isCompleted;
final TaskPriority priority;
final DateTime? deadline;
final DateTime createdAt;
final String? note;
Task({
required this.id,
required this.title,
this.isCompleted = false,
this.priority = TaskPriority.medium,
this.deadline,
required this.createdAt,
this.note,
});
Task copyWith({
String? title,
bool? isCompleted,
TaskPriority? priority,
DateTime? deadline,
String? note,
}) {
return Task(
id: id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
priority: priority ?? this.priority,
deadline: deadline ?? this.deadline,
createdAt: createdAt,
note: note ?? this.note,
);
}
factory Task.fromJson(Map<String, dynamic> json) {
return Task(
id: json['id'] as String,
title: json['title'] as String,
isCompleted: json['isCompleted'] as bool? ?? false,
priority: TaskPriority.values[json['priority'] as int? ?? 1],
deadline: json['deadline'] != null
? DateTime.parse(json['deadline'] as String)
: null,
createdAt: DateTime.parse(json['createdAt'] as String),
note: json['note'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'isCompleted': isCompleted,
'priority': priority.priorityScore,
'deadline': deadline?.toIso8601String(),
'createdAt': createdAt.toIso8601String(),
'note': note,
};
}
}
这里有个非常关键的细节:deadline 我用了 DateTime? 可空类型。因为用户很可能不设置截止日期,而“无截止日期排最后”正是排序规则中最容易写错的地方。toJson 方法里用 toIso8601String() 保存时间,是为了避免不同平台对时间格式的解析差异。
顺带说一句代码组织的问题。Dart 支持 part 关键字拆分库文件,但我个人不太建议在业务模型这种小文件上用 part。待办项目里 Task 模型就几十行,拆成多个 part 文件只会让调试时多一层跳转。核心业务代码尽量保持“一个类一个文件、直接可见”,这是我在这个项目里踩过代码组织坑后得出的经验。
2.2 排序比较器:稳定、可解释、防呆
排序函数是整个项目最核心的逻辑。我用了一个独立的纯函数 sortTasks,入参是原始列表,出参是排好序的新列表。注意我特意没有在原地修改原列表,而是用 List.from 复制了一份,这样可以避免 UI 层的数据引用被意外污染。
dart复制enum SortMode {
priority,
deadline,
createdAt,
}
/// 带空值处理的截止日期比较器
/// 返回负数表示 [a] 排在 [b] 前面
int _compareDeadline(DateTime? a, DateTime? b) {
if (a == null && b == null) return 0;
if (a == null) return 1; // a 没有截止日期,排后面
if (b == null) return -1; // b 没有截止日期,排后面
return a.compareTo(b);
}
List<Task> sortTasks(List<Task> tasks, {SortMode mode = SortMode.priority}) {
final sorted = List<Task>.from(tasks);
sorted.sort((a, b) {
// 第一排序键:未完成任务优先,已完成任务沉底
if (a.isCompleted != b.isCompleted) {
return a.isCompleted ? 1 : -1;
}
switch (mode) {
case SortMode.priority:
// 第二排序键:优先级权重高优先
final priorityCmp = a.priority.priorityScore
.compareTo(b.priority.priorityScore);
if (priorityCmp != 0) return priorityCmp;
// 第三排序键:截止日期近优先,无截止日期排最后
final deadlineCmp = _compareDeadline(a.deadline, b.deadline);
if (deadlineCmp != 0) return deadlineCmp;
// 第四排序键:创建时间新优先
return b.createdAt.compareTo(a.createdAt);
case SortMode.deadline:
return _compareDeadline(a.deadline, b.deadline);
case SortMode.createdAt:
return b.createdAt.compareTo(a.createdAt);
}
});
return sorted;
}
这个函数的几个设计要点值得展开讲。
比较器必须满足反对称性和传递性。 这是排序算法正确运行的前提。直观地说:如果 A 排在 B 前面,那 B 肯定排在 A 后面;如果 A 排在 B 前面、B 排在 C 前面,那 A 必须在 C 前面。我在实际 review 代码时经常看到有人把 a.compareTo(b) 和 b.compareTo(a) 混着用,导致排序结果不稳定。上面这个函数,每个维度都保持方向一致,不会出现这种问题。
“未完成沉底”不是 UI 效果,而是排序函数的第一排序键。 你会发现不管用户选择哪种排序模式,已完成任务都是排在最后的。这是有意的产品决策:待办列表的核心使用场景是“处理未完成事项”,已完成任务默认折叠底部,用户需要时主动往下翻就能看到,而不是混在未完成事项里干扰视线。
_compareDeadline 单独抽出来处理空值。 这是一个新手极容易踩的坑。如果你直接写 a.deadline!.compareTo(b.deadline!),遇到空值就会崩溃;如果你写 a.deadline?.compareTo(b.deadline) ?? 0,空值又会被当成相等,导致“无截止日期”的任务穿插在“有截止日期但很远”的任务前面。这个辅助函数把“空值排最后”的规则集中处理了。
Dart 的 List.sort 在这个场景下还有一个实际好处:它要求比较器必须定义全序关系,上面这个比较器对任意两个任务都能给出明确顺序。当你切换到“按截止日期”模式时,未完成任务会先按截止日期排,已完成任务依然沉底,这个行为对用户来说是可预期的。
2.3 状态管理联动:Cubit 事件流与列表重排
排序函数写好之后,接下来要解决的是“什么时候触发排序”。我强烈建议:不要在前端 UI 层排序,而是在状态管理层的 emit 之前完成排序。这样排序逻辑可以单测,UI 层只负责展示最终结果。
我用 Cubit 管理状态,事件和状态的定义如下:
dart复制sealed class TaskEvent {}
class AddTaskEvent extends TaskEvent {
final Task task;
AddTaskEvent(this.task);
}
class ToggleTaskEvent extends TaskEvent {
final String taskId;
ToggleTaskEvent(this.taskId);
}
class DeleteTaskEvent extends TaskEvent {
final String taskId;
DeleteTaskEvent(this.taskId);
}
class ChangeSortModeEvent extends TaskEvent {
final SortMode mode;
ChangeSortModeEvent(this.mode);
}
对应的 Cubit:
dart复制class TaskCubit extends Cubit<TaskState> {
TaskCubit(this._repository) : super(const TaskState()) {
_load();
}
final TaskRepository _repository;
Future<void> _load() async {
final tasks = await _repository.loadTasks();
final sorted = sortTasks(tasks, mode: state.sortMode);
emit(state.copyWith(tasks: sorted));
}
void addTask(Task task) {
final tasks = sortTasks([...state.tasks, task], mode: state.sortMode);
emit(state.copyWith(tasks: tasks));
_repository.saveTasks(tasks);
}
void toggleTask(String taskId) {
final updated = state.tasks
.map((t) => t.id == taskId ? t.copyWith(isCompleted: !t.isCompleted) : t)
.toList();
final sorted = sortTasks(updated, mode: state.sortMode);
emit(state.copyWith(tasks: sorted));
_repository.saveTasks(sorted);
}
void deleteTask(String taskId) {
final tasks = state.tasks.where((t) => t.id != taskId).toList();
emit(state.copyWith(tasks: tasks));
_repository.saveTasks(tasks);
}
void changeSortMode(SortMode mode) {
final sorted = sortTasks(state.tasks, mode: mode);
emit(state.copyWith(tasks: sorted, sortMode: mode));
}
}
看到没有?每次状态变更之后都调用 sortTasks,而不是只在添加任务时排序。这是个很容易忽略的点:你新增了一个高优先级任务,它插到了列表最前面;这时候如果把另一个中等优先级任务标记为完成,它的位置会因“已完成沉底”规则发生移动,如果不重新排序,列表就乱了。把排序收敛在 Cubit 里,每次事件处理都有明确输出,前端就永远不会出现“列表顺序与排序规则不符”的诡异情况。
UI 层我用了 BlocBuilder 监听状态变化:
dart复制BlocBuilder<TaskCubit, TaskState>(
builder: (context, state) {
final tasks = state.tasks;
if (tasks.isEmpty) {
return const Center(child: Text('暂无待办,点击右下角添加'));
}
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return Dismissible(
key: ValueKey(task.id),
onDismissed: (_) =>
context.read<TaskCubit>().deleteTask(task.id),
child: ListTile(
leading: Checkbox(
value: task.isCompleted,
onChanged: (_) =>
context.read<TaskCubit>().toggleTask(task.id),
),
title: Text(task.title),
subtitle: Text(_buildSubtitle(task)),
trailing: Text(
task.priority.label,
style: TextStyle(color: _priorityColor(task.priority)),
),
),
);
},
);
},
)
这里有一个小细节:ListView.builder 的 itemBuilder 里,每个列表项的 key 必须用 task.id,不能用 index。因为排序变化后,同一位置的 item 会变成另一个任务,如果用 index 作为 key,Flutter 复用元素时状态会串掉——滚动位置错乱还是小事,更麻烦的是 checkbox 的选中状态可能显示在错误的任务上。
3. 环境搭建与鸿蒙适配全流程
3.1 下载适配版 Flutter SDK,版本匹配是硬规矩
Flutter 官方主分支目前没有鸿蒙目标,你要用的是 OpenHarmony SIG 维护的 flutter_flutter 分支。下载地址在 SIG 的代码仓库(gitee 的 openharmony-sig/flutter_flutter),下载后把 bin 目录加进 PATH 即可,和官方 Flutter 的配置方式一样。
这里有个非常非常重要的提示:一定要按你下载的分支 README 里标注的版本来安装配套的 DevEco Studio 和 SDK。这个分支跟鸿蒙系统能力绑定很紧,API 版本错位直接导致编译失败。我自己第一次装的时候图省事,拿最新的 DevEco Studio 4.x 去配一个旧版本的适配分支,结果编译报错,查了半天才发现是版本不匹配。别再踩这个坑了,老老实实看 README 的版本对照表。
安装完 DevEco Studio 后,需要在里面配置 HarmonyOS SDK,建议把 API 10 或更新的稳定版勾上。到这里,开发环境就算基本齐了。
3.2 创建 Flutter 工程,添加鸿蒙构建入口
创建工程和平常一样:
bash复制flutter create todo_app
建完之后,工程里会看到 android/、ios/ 等目录。鸿蒙平台的构建入口是 ohos/ 目录,需要你手动通过适配工具链或者直接将工程导入 DevEco Studio 来补充。
具体流程不同版本略有差异,我现在的做法是:在 DevEco Studio 里直接打开 Flutter 工程,它会识别 Flutter 模块并自动处理 ohos 工程结构。然后在 DevEco Studio 里选择“运行”或“构建 HAP”,它会调用 Flutter 适配引擎完成编译。
如果你不想用 IDE 跑完整流程,也可以直接在命令行先构建出 Flutter 的产物,再交给 DevEco Studio 打 HAP 包。无论哪种方式,你要记住一个原则:Dart 代码是跨平台共享的,但打包成鸿蒙应用这一步必须在鸿蒙工具链里完成。
3.3 真机调试:从 hdc 连接到签名打包
真机调试这块,我有几条实测经验。
鸿蒙系统的设备调试用的是 hdc 命令,它相当于 Android 世界的 adb。设备开启开发者模式并打开 USB 调试后,用数据线连上电脑,先执行:
bash复制hdc list targets
能看到设备就说明连接正常,然后 hdc shell 就能进到设备的命令行。
这里有个 Linux 下特有的坑:如果你用的是 Linux 环境,hdc list targets 可能看不到设备,因为系统没有给 hdc 配置 USB 权限。解决方法是手动添加 udev 规则,把鸿蒙设备的 USB VendorID 加到规则文件里,具体配置方法网上都能查到,我不展开写了,但你如果遇到“设备连不上”的问题,十有八九是这个原因。
连接上设备后,关键一步是签名。鸿蒙应用安装到真机上需要签名信息,DevEco Studio 提供了一个“自动签名”的选项,用华为账号登录后会自动生成签名文件。如果你没配置签名,真机运行时会被直接拒绝安装。模拟器相对宽松一些,但模拟器在 GPU 渲染上和真机有差异,跑 Flutter 这种自绘引擎的应用时,我建议核心功能尽量在真机上验证,尤其是列表排序后滚动这种交互,模拟器上流畅不代表真机没问题。
另外回答一个很多人问的问题:如果手头没有鸿蒙手机,能不能开发调试?可以,DevEco Studio 自带模拟器,支持手机、平板等常见形态,Flutter 工程可以直接跑在模拟器上。逻辑验证、UI 调整都没问题,等真正需要验收性能时再上真机。
4. 实操中的坑与排查速查表
4.1 编译与运行阶段的三个高频报错
这个项目我踩过的编译报错,排在前三的基本都是环境问题。
第一个是 The current configured Flutter SDK is not known to be fully supported. Please 这类提示。这个报错的意思是:当前配置的 Flutter SDK 版本不在工具链支持列表里,常见原因是 flutter_flutter 分支版本和你本地的工具链标签对不上。处理方法也很直接——回到 README 指定的版本组合上。如果你确定版本组合没问题但依然报错,在项目根目录检查 .metadata 文件里的 version 字段,手工对齐即可。
第二个是和 Impeller 渲染引擎相关的异常。新版本 Flutter 默认启用 Impeller 渲染,但鸿蒙适配分支对 Impeller 的支持还在完善中,真机运行可能出现白屏、贴图异常,甚至直接闪退。如果遇到这种渲染问题,先用 flutter run --no-enable-impeller 跑一遍,如果恢复正常,说明问题在渲染引擎层面而不是业务代码。这个开关是你做鸿蒙适配时的第一排查工具。
第三个是依赖拉取失败。Flutter 官方依赖源在部分网络环境下不太稳定,国内开发环境建议把 PUB_HOSTED_URL 和 FLUTTER_STORAGE_BASE_URL 配置到国内镜像源。镜像源本身不会让你少踩坑,但至少能把“依赖拉不下来”这个问题排除掉,避免误判成代码问题。
4.2 插件适配对照与平台通道
纯 Flutter 代码在鸿蒙上问题不大,真正麻烦的是原生插件。因为 flutter_flutter 分支要跑在 OpenHarmony 上,插件必须提供对应的鸿蒙原生实现,官方 pub.dev 上的很多插件不会直接支持鸿蒙。
我在这个项目里用到的插件不多,但即使是这样也已经感受到了适配包的问题:
| 官方包 | 鸿蒙适配包 | 用途 |
|---|---|---|
| shared_preferences | shared_preferences_ohos | 轻量 KV 存储,本项目的主要落点 |
| path_provider | path_provider_ohos | 获取应用目录路径 |
| sqflite | sqflite_ohos | SQLite 数据库,数据量大时替换 |
做法是在 pubspec.yaml 里分别声明各平台依赖。这些适配包通常保持了与原包一致的接口,所以 Dart 层代码几乎不用改。如果某个第三方包找不到鸿蒙适配版本,你有两个选择:替换成功能类似的通用方案,或者通过平台通道自己在鸿蒙侧实现一套原生接口。
顺带说一下平台通道。Flutter 在鸿蒙上同样支持 MethodChannel,Dart 层通过通道调用鸿蒙原生能力,鸿蒙侧在 Stage 模型里用 ArkTS 实现方法调用。待办应用如果要接入系统闹钟做精确提醒,就需要走这个通道。Dart 层代码不变,Android 端和鸿蒙端各自的原生实现写一遍即可。
4.3 列表与交互层面的隐形坑
这节内容不是环境问题,而是纯开发问题,但却是最容易让用户觉得“不顺手”的地方。
第一个是排序模式切换时列表的“跳动感”。当你把排序模式从“按优先级”切到“按截止日期”时,任务顺序会发生剧烈变化,用户视角就是列表整个跳了一下,视觉体验很差。我的解法是在切换排序模式后让列表滚动回顶部,同时用动画过渡:
dart复制void changeSortMode(SortMode mode) {
final sorted = sortTasks(state.tasks, mode: mode);
emit(state.copyWith(tasks: sorted, sortMode: mode));
// UI 层监听 sortMode 变化,用 AnimatedList 或者简单的滚动到顶部
}
敬告:不要以为列表顺序变了就完事,排序切换后必须处理滚动位置,否则体验很“业余”。
第二个是键盘遮挡问题。在添加待办的输入框或者在编辑截止日期时,弹出的软键盘会遮住输入区域。这个在 Android 和鸿蒙上表现还不太一样,鸿蒙某些版本上默认不会自动调整滚动位置。我的做法是给输入场景套一个 SingleChildScrollView,并在 TextEditingController 的焦点变化时做一次 ensureVisible 滚动。
第三个是时间存储的时区问题。DateTime 序列化后存储的是 UTC 时间,不同设备时区不同,如果读出来直接用本地时间展示,截止日期会差几个小时。我的建议是存储时统一用 UTC(toUtc().toIso8601String()),展示时再转成本地时区。这个小细节不改的话,排序结果在跨时区设备上会出现“明明还有一天,却被排到了最前面”的诡异现象。
5. 测试、打包与后续扩展
5.1 排序逻辑的单元测试:纯函数的优势
排序这种“输入列表,输出有序列表”的逻辑,天然适合单元测试。我用 flutter_test 写了下面几组用例:
dart复制import 'package:flutter_test/flutter_test.dart';
import 'package:todo_app/models/task.dart';
import 'package:todo_app/utils/sort_tasks.dart';
void main() {
group('sortTasks 按优先级排序', () {
test('未完成任务排在已完成任务之前', () {
final done = Task(
id: '1',
title: '已完成任务',
isCompleted: true,
createdAt: DateTime(2025, 1, 1),
);
final todo = Task(
id: '2',
title: '未完成任务',
createdAt: DateTime(2025, 1, 1),
);
final sorted = sortTasks([done, todo]);
expect(sorted.first.id, '2');
});
test('高优先级排在低优先级之前', () {
final low = Task(
id: 'low',
title: '低优先级',
priority: TaskPriority.low,
createdAt: DateTime(2025, 1, 1),
);
final high = Task(
id: 'high',
title: '高优先级',
priority: TaskPriority.high,
createdAt: DateTime(2025, 1, 1),
);
final sorted = sortTasks([low, high]);
expect(sorted.first.id, 'high');
});
test('截止日期早的排在晚的前面', () {
final later = Task(
id: 'later',
title: '截止日期晚',
deadline: DateTime(2025, 3, 1),
createdAt: DateTime(2025, 1, 1),
);
final earlier = Task(
id: 'earlier',
title: '截止日期早',
deadline: DateTime(2025, 2, 1),
createdAt: DateTime(2025, 1, 1),
);
final sorted = sortTasks([later, earlier], mode: SortMode.deadline);
expect(sorted.first.id, 'earlier');
});
test('无截止日期的任务排在有截止日期的任务后面', () {
final noDeadline = Task(
id: 'no-deadline',
title: '无截止日期',
createdAt: DateTime(2025, 1, 1),
);
final withDeadline = Task(
id: 'with-deadline',
title: '有截止日期',
deadline: DateTime(2025, 9, 1),
createdAt: DateTime(2025, 1, 1),
);
final sorted = sortTasks([noDeadline, withDeadline]);
expect(sorted.first.id, 'with-deadline');
});
});
}
跑单测的命令和平常一样:
bash复制flutter test
我做完这个项目之后,最大的感受是:排序逻辑越重要,越要用单测保护起来。待办的排序规则一旦被破坏,用户不会在第一时间告诉你“排序错了”,他们只会觉得“这个应用用着别扭”,然后默默流失。单测至少能保证改动其他功能时不会把排序搞坏。
5.2 跨平台打包的差异与产物管理
完成了排序和适配之后,最后就是打包发布。三个端口的产物形态不一样,这点要提前有预期。
- Android:
flutter build apk或flutter build appbundle,产物分别是 APK 和 AAB。 - iOS:
flutter build ios,项目里提一下,实际需要在 macOS 环境下完成。 - 鸿蒙:通过 DevEco Studio 构建 HAP 包,这是鸿蒙应用的安装包格式。构建过程会把 Flutter 引擎产物集成进去,最终打包出可安装的 HAP。
需要提醒的是:如果你的项目同时维护 Android 和鸿蒙,依赖的三方包要特别注意版本锁定。我在开发过程中踩过“Android 上正常、切到鸿蒙构建时因为依赖版本冲突导致编译失败”的坑,建议 pubspec.yaml 里对关键依赖直接锁定版本号,不要用 ^ 范围版本到处飘。
5.3 后续功能扩展的方向
这个项目做完基础版之后,如果你想继续往下走,有几个方向天然和“优先级排序”兼容:
- 拖拽排序:允许用户手动调整同优先级任务的顺序,把“手动顺序”作为排序函数的最高权重维度。实现上可以用
ReorderableListView,但注意拖拽结果要同步到持久化层。 - 优先级分组:不展示扁平列表,而是按“高、中、低”分成三个区块,每个区块内部再按截止日期排序。UI 上用
CustomScrollView配合 slivers 实现,体验更好。 - 提醒通知:给任务设置提醒时间,到点通过本地通知推送给用户。这里的重点是鸿蒙侧的本地通知通道要单独适配。
- TTS 语音播报:结合 TTS 把当前最紧急的待办事项读出来,适合通勤场景。Dart 层写好逻辑,各平台接入对应的 TTS 插件即可。
最后一个建议:如果你打算把 Flutter 鸿蒙应用正式发布,一定多在不同设备上过几遍核心流程。flutter_flutter 分支更新很快,每次升级 SDK 之后都要重新回归一遍待办的增删改查和排序功能,这比加多少新功能都重要。排序这种细活,用户是能感受到的。
