写这篇鸿蒙化适配指南之前,先说说我为什么会对 error_or 这个库上心。做 Flutter 应用这几年,业务逻辑里最磨人的不是 UI 渲染,而是那些散落在各个回调里的错误分支:网络超时、参数非法、本地缓存穿透、权限被拒……每个地方都要手写 if-else 判断,写多了代码就变臭。正好前阵子公司开始推进鸿蒙化,需要把一批 Flutter 三方库迁到 OpenHarmony 平台上,error_or 就是我重点处理的第一个纯 Dart 库。这篇文章把整个适配过程、踩坑记录和业务接入后的效果都整理出来,给同样在做 Flutter 鸿蒙化的团队一个可参考的样板。
先说结论:error_or 这类纯 Dart 的函数式错误处理库,鸿蒙化成本比原生插件低得多,基本不需要改 Dart 代码,但你得把依赖解析、构建验证和业务接入方式搞清楚,才能真正发挥它“流式错误处理”的价值。
1. 为什么需要流式错误处理:error_or 的核心价值
1.1 传统错误处理的痛点
Flutter 应用里的错误处理,最传统的写法就是 try-catch 加返回值判断。比如请求用户信息时,要先检查网络是否正常,再检查返回数据是否为空,再解析 JSON 是否有异常,三层嵌套下来,代码就变成这样:
Future<UserProfile?> fetchUserProfile() async { try { final response = await httpClient.get('/user/profile'); if (response.statusCode != 200) { return null; } final json = jsonDecode(response.body); if (json['data'] == null) { return null; } return UserProfile.fromJson(json['data']); } catch (e) { // 记日志 return null; } }这个方法的调用方拿到 null 之后,根本不知道是网络问题、数据格式问题还是服务端返回了异常。更麻烦的是, null 本身就是合法的业务值,比如用户确实没设置头像时 data 字段就可能是 null。你用 null 表示错误,就等于把“用户没有头像”和“请求失败”这两种完全不同的情况混在了一起,后面排查问题只能靠猜。
还有一种方案是用自定义异常 + catch 分支,但异常会打断函数执行的正常流,不适合表达“每一步都可能出错”的链式逻辑。比如你要先校验参数,再查缓存,再走网络,三个步骤必须串在一起,每个步骤都可能产生不同类型的错误。用 try-catch 写出来就像一大段面条代码,可读性极差。
1.2 error_or 是什么:从 Either 到 ErrorOr
error_or 本质上借鉴了函数式编程里的 Either 类型。它提供的核心类型叫 ErrorOr<T>,一个对象要么承载正常值,要么承载错误信息,不会同时为空。这样你就不用再用 null 或 -1 这种魔法值去表达异常了。
跟 Dart 社区常见的 Either 类型相比,error_or 有几个针对 Flutter 场景的优化:
- 它把错误设计成可扩展的数据结构,而不是只能塞一个字符串。你可以把错误码、错误消息、底层异常、堆栈、额外上下文都放进去。
- 它提供了丰富的链式操作符,比如 map、flatMap、onFailure、recover,适合流式的错误处理链路。
- 它支持异步版本,配合 async/await 用起来很自然,不像 RxDart 那样需要引入一整套响应式框架。
举一个实际用法例子。改造后的 fetchUserProfile:
Future<ErrorOr<UserProfile>> fetchUserProfile() async { final response = await httpClient.get('/user/profile'); if (response.statusCode != 200) { return ErrorOr.failure( businessError: BusinessError( code: 'HTTP_${response.statusCode}', message: '用户接口返回异常', ), ); } final json = jsonDecode(response.body) as Map<String, dynamic>; return ErrorOr.value(UserProfile.fromJson(json)); }调用方就可以这样处理:
final result = await userRepository.fetchUserProfile(); result.when( (profile) => _renderProfile(profile), (error) => _showErrorToast(error.message), );一眼就能看出成功和失败两条路径,不会再混淆“没数据”和“出错了”。而且你可以把多个 ErrorOr 串联成一组操作,错误会自动短路,不需要手动判断每一步的返回值。
1.3 适合谁用
如果你正在做鸿蒙化改造,同时又要保证业务代码的稳定性和可维护性,那这个库值得你花半天时间接进来。它特别适合这几类场景:
- 统一封装网络层、数据库层、文件 IO 层的错误反馈,让上层 UI 不用关心底层细节。
- 需要在用户界面上展示“优雅的失败提示”,而不是弹出一段丑陋的堆栈。
- 需要在团队内推行一套标准的错误码规范,减少不同开发之间随意的字符串错误。
- 正在从 null 判断和异常捕获混用的旧代码,迁移到更结构化的错误处理模型。
当然,如果你的项目只有一两个接口,或者团队里没人熟悉函数式概念,那引入 error_or 的收益没那么明显。但它作为库本身非常轻量,没有原生依赖,这就为鸿蒙化打下了很好的基础。
2. 鸿蒙化适配准备:从 Flutter 工程到 OpenHarmony SDK
2.1 适配前的环境清单
要把一个 Flutter 三方库适配到鸿蒙,首先得清楚你的目标到底是什么。鸿蒙端的 Flutter 支持走的是 OpenHarmony 社区维护的 Flutter SDK 分支,这套 SDK 基本沿用了标准 Flutter 的项目结构,但增加了 ohos 平台目录和对应的构建产物。我用的是下面这套环境,写出来给你参考:
| 组件 | 版本 / 说明 |
|---|---|
| OpenHarmony SDK | API 10 及以上,建议直接用 DevEco Studio 自带的 SDK |
| Flutter SDK for OpenHarmony | 建议用社区 3.7+ 的分支,跟随上游 Flutter 版本更新 |
| Dart SDK | 跟随 Flutter SDK,无需单独安装 |
| DevEco Studio | 4.0 以上的版本,用于编译鸿蒙原生壳 |
| 构建工具 | hvigor,随 DevEco Studio 分发 |
这里要提醒一下,别把鸿蒙的 Flutter SDK 和官方 Flutter SDK 混用。鸿蒙版 Flutter 会在编译时检查目标平台,如果你用官方 Flutter 构建 ohos 目标,会报找不到 ohos 平台的错误。我当时就是没注意,直接在一个老项目里跑flutter build ohos,结果折腾了半天才发现是 SDK 分支不对。
2.2 判断依赖是纯 Dart 还是含原生代码
鸿蒙化适配的难度,完全取决于你要适配的包是不是纯 Dart。如果包里没有任何原生平台代码,理论上在鸿蒙 Flutter 工程里把它当作普通 pub 依赖就行,编译期和运行期都不会牵扯到 Android 或 iOS 的 API。error_or 就是这种情况,它没有依赖 flutter/services,也没有 plugin 注册逻辑。
反过来,如果包内部用了dart:ffi、package_info_plus这类平台通道,那就需要为鸿蒙单独实现插件接口,工作量会大很多。所以适配的第一步,就是确认包的纯净度。
怎么快速判断?打开包的源码目录看一下:
ls ~/.pub-cache/hosted/pub.dev/error_or-*/如果发现里面有 android/、ios/ 这样的目录,说明它带原生组件。error_or 我看了下,只有 lib/ 目录和 pubspec.yaml,没有任何平台插件目录。
再进一步,检查 pubspec.yaml 中的 dependencies,error_or 只依赖了 meta 或者更小的基础包,没出现 flutter/services,也没有 plugin_class 声明。这种包在鸿蒙上几乎不会遇到“找不到原生实现”的问题。
2.3 一个关键的 pubspec 检查
很多人在适配时忽略了依赖传递问题。error_or 本身适配容易,但它依赖的上游包如果有平台特性,就会间接影响你。我当时的做法是,先列出完整依赖树,确认没有隐藏的平台通道依赖:
flutter pub deps --style=compact输出结果里能看到 error_or 的依赖项。只要这些依赖不涉及 dart:ui 之外的平台能力,就可以放心构建。
另外还要注意 Dart SDK 版本兼容。鸿蒙版 Flutter 的 Dart SDK 版本一般比官方稳定版落后一点点,而 error_or 可能用到了较新的语法。如果编译报语法错误,要么升级鸿蒙 Flutter SDK,要么在 pubspec 里锁定一个兼容版本。这个坑在第 3 节详细讲。
3. 鸿蒙化适配实操:三步让 error_or 跑起来
3.1 第一步:建立鸿蒙 Flutter 工程骨架
如果你已经有一个要鸿蒙化的 Flutter 应用,这一步可以跳过。如果是从零开始,推荐用鸿蒙版 Flutter SDK 自带的 flutter create 命令创建工程,它会自动生成 ohos 目录。命令大概是:
flutter create --platforms=ohos,android,ios -e my_app注意--platforms参数里要带上 ohos,而且你当前 PATH 里的 flutter 命令必须指向鸿蒙版 SDK。创建成功后,工程目录结构大致是:
my_app/ lib/ ohos/ android/ ios/ pubspec.yamlohos 目录相当于一个完整的鸿蒙原生壳工程,里面是 ets、entry、build-profile 等文件。后续调试时,用 DevEco Studio 打开 ohos 目录,就能跑通鸿蒙原生部分。
3.2 第二步:调整依赖声明与版本约束
接下来处理 pubspec.yaml。通常你想把 error_or 安装到项目里,直接:
flutter pub add error_or这样做的结果是在 dependencies 里生成一行error_or: ^x.y.z。对于纯 Dart 包,鸿蒙 Flutter 和标准 Flutter 同样使用 pub 仓库,依赖解析规则没有本质区别。
但在实际操作中,我遇到过两种情况需要手动干预:
第一种是鸿蒙 Flutter SDK 的 Dart 版本较旧,而 error_or 新版要求更高的 Dart 版本。此时 pub 会提示版本兼容冲突,解决方式就是把 error_or 锁定到旧版本,比如error_or: 1.4.2。锁定后重新flutter pub get,看看是否有其他冲突。
第二种是如果你使用私有 pub 仓库或镜像,需要确认 error_or 是否同步到了你的仓库。我当时因为公司内网用私有仓库,pub get 一直失败,换成官方仓库源才成功。这个问题在鸿蒙化团队里挺常见的,尤其是有多个镜像源的时候。
再提一个点:鸿蒙化 Flutter 项目里,pubspec.yaml 中可能还需要添加一个dependency_overrides来对齐鸿蒙 SDK 需要的特定版本库,比如sky_engine或flutter的版本。一般情况下不用,但如果你发现编译时出现“flutter SDK 版本不匹配”的警告,就可以用这个方式兜底。
3.3 第三步:构建、运行与单元测试验证
依赖装好之后,先别急着写业务,先跑一次构建验证。鸿蒙 Flutter 工程的构建命令是:
flutter build ohos --debug如果构建过程中没有报错,那 error_or 在编译期已经确认兼容。接下来做单元测试验证。error_or 的逻辑不依赖 UI,非常适合跑 dart 单测。我一般在适配后补一个最小的测试用例,保证原有代码逻辑在鸿蒙环境下没有行为变化。
test('ErrorOr should hold value', () { final result = ErrorOr.value(42); expect(result.isSuccess, isTrue); expect(result.value, 42); });运行:
flutter test test/error_or_smoke_test.dart这一步能快速定位 Dart 运行时行为是否跟标准 Flutter 一致。我在测试时发现鸿蒙上的 Dart VM 对 stack trace 的处理有些差异,但 ErrorOr 的纯逻辑不受影响,测试顺利通过。
最后,用真机或模拟器跑一次包含 error_or 的小页面,重点验证异步链路的错误触发是否正常。这里可以参考鸿蒙开发里的常规做法:用 DevEco Studio 连接模拟器,然后在 Flutter 侧flutter run -d <device-id>启动应用。
到这里,error_or 的鸿蒙化适配就算完成一大半了。接下来才是真正的重头戏:怎么把它用在业务里,提升应用的业务反馈质量。
4. 业务反馈质量实战:用 ErrorOr 改造异步流
4.1 改造前后对比
我这里拿一个典型的“用户登录后拉取初始化数据”场景举例。登录成功之后,客户端要并行请求用户资料、应用配置、未读消息数三个接口。任何一个接口失败,都不能让用户感觉“白登录了”,而是要在页面上给出清晰、可恢复的提示。传统写法是这样的:
Future<void> loadInitData() async { setState(() => _loading = true); try { final profileFuture = fetchUserProfile(); final configFuture = fetchAppConfig(); final messageFuture = fetchUnreadCount(); await Future.wait([profileFuture, configFuture, messageFuture]); // 真正用数据时,还要小心哪些接口返回了 null } catch (e) { _showRetryDialog('初始化失败'); } finally { setState(() => _loading = false); } }问题很明显:catch 只能捕获第一个抛出的异常,另外两个接口万一也失败,根本不知道。而且Future.wait默认是“全部成功才算成功”,如果其中一个失败,其他成功的数据也会跟着丢弃。用 error_or 改造后,每个接口单独返回 ErrorOr,再逐个判断,既能局部失败,也能整体聚合。
4.2 构建统一的业务错误模型
要让 ErrorOr 真正发挥作用,不能只把它当箱子装字符串。我建议先在项目里建一个统一的错误模型,放在 core/error 目录下:
sealed class AppError { const AppError({required this.code, required this.message}); final String code; final String message; } class NetworkError extends AppError { const NetworkError({required String message}) : super(code: 'NETWORK', message: message); } class ApiError extends AppError { const ApiError({required String statusCode, required String message}) : super(code: 'API_$statusCode', message: message); } class BizError extends AppError { const BizError({required String code, required String message}) : super(code: code, message: message); }然后给 error_or 定义数据的包装类型,比如ErrorOr<UserProfile> result = ErrorOr.tryCatch(() => ..., onError: (e) => AppError.fromException(e));
这样 error_or 里承载的错误永远是我们业务认识的 AppError,而不是底层异常类型。UI 层只需要面向 AppError 展示文案和重试逻辑。这个模型的另一个好处是,鸿蒙侧的日志服务可以统一读取 code 和 message,不用解析异常对象。
4.3 与 EventChannel 结合上报错误流
刚才提到的热搜词里有 flutter 组件通信和 eventchannel,这其实跟错误处理有关系。在鸿蒙应用里,Flutter 页面和鸿蒙原生页面经常需要互相通信。比如你把错误抛给了 ErrorOr,但用户停留在原生页面,你需要让原生页面也能感知到这个错误,并弹出一个鸿蒙原生的提示框。这时候可以借助 Flutter 的 EventChannel,把 ErrorOr 里的错误信息流式推送到鸿蒙侧。
具体做法是,在 Flutter Dart 侧定义一个平台通道:
const _errorStreamChannel = EventChannel('com.example.app/error_stream'); void startErrorStream() { _errorStreamChannel.receiveBroadcastStream().listen((event) { // 接收来自原生侧的错误事件 final errorMap = event as Map<dynamic, dynamic>; handleNativeError(errorMap); }); } void sendErrorToNative(AppError error) { const methodChannel = MethodChannel('com.example.app/error_channel'); methodChannel.invokeMethod('reportError', {'code': error.code, 'message': error.message}); }鸿蒙侧是 ets 的 Module 里注册 EventChannel 对应的 handler。思路很简单:Dart 侧触发业务错误之后,先交给 ErrorOr 统一处理,再把错误模型序列化之后推给原生侧。这样做的好处是错误处理逻辑全部集中在 Dart 层,原生层只负责展示和上报,职责单一。
我实测下来,这个方案比在原生侧重复处理错误要高效得多。原来坏了参数、超时、缓存读取失败都要在两边各写一套判断,现在只需要在 Dart 侧用 ErrorOr 链式处理,然后按需推流。用户看到的反馈提示也更统一了,不会再出现“Flutter 页面提示 A,原生页面提示 B”的割裂感。
4.4 流式错误处理的链式写法
ErrorOr 真正的威力在于链式组合。比如你加载用户详情时,需要先读本地缓存,缓存没有则走网络,网络成功再写缓存。每一步都可能失败,但失败的原因不同:
Future<ErrorOr<UserDetail>> loadUserDetail(int userId) async { final fromCache = await _cache.load(userId); if (fromCache.isSuccess) { return fromCache; } final fromNet = await _api.fetchUserDetail(userId); return fromNet.flatMap((detail) async { await _cache.save(userId, detail); return ErrorOr.value(detail); }).recover((error) { // 缓存和网络都失败时,尝试用本地兜底副本 return _localBackup.loadSafely(userId); }); }recover这个操作很关键,它让错误不再是一句“完了”,而是一个可以挽救的机会。你可以把“缓存失败”这个错误捞起来,继续尝试网络;甚至可以把可恢复错误重新包装成一种“降级成功”的状态,UI 上可以提示“当前为离线数据”。
这种写法相比层层 try-catch,最大的区别是错误被当作数据流中的一个节点,而不是程序崩溃的导火索。在鸿蒙应用里,用户对“反馈质量”的感知往往很敏感,如果网络不好就弹一个干巴巴的“网络错误”对话框,很容易被打低分。用 ErrorOr 可以精确控制“什么时候该提示、什么时候该静默重试、什么时候该降级”,这些逻辑都可以用一行链式调用表达清楚。
5. 常见问题与避坑经验
5.1 版本兼容引发的“类型不匹配”
鸿蒙 Flutter SDK 的 Dart 版本通常不是最新,这就导致 error_or 新版本里用到的Uri解析或者集合操作 API 可能与旧版 Dart 不一致。我当时适配时遇到一个很奇怪的问题:代码能通过编译,但运行时 error_or 内部抛出的断言错误一直显示“类型不匹配”。
排查后发现,问题不在 error_or 本身,而在我项目里同时引用了另一个依赖,把 error_or 依赖的 meta 包升级到了一个不兼容的版本。pub 的依赖解析器在鸿蒙分支上会有不同的冲突处理策略,它会选中一个看似兼容但实际 ABI 不一致的版本。
建议做法:在 pubspec.yaml 中明确锁定 error_or 及其上游 key 依赖,尽量不要用^范围升级。比如这样:
dependencies: error_or: 2.1.0 meta: 1.9.0如果项目里必须依赖高版本 meta,再用dependency_overrides统一覆盖。不要嫌麻烦,鸿蒙生态里的 pub 解析本来就不如官方 Flutter 成熟,多锁定一层少一个隐患。
5.2 热重载后 ErrorOr 状态丢失
鸿蒙版 Flutter 的热重载(Hot Reload)整体可用,但如果你在 ErrorOr 里保存了某些涉及原生资源的对象,比如数据库连接通道、平台通道回调,热重载之后可能会出现“上次的错误状态被清空”的假象。
这是因为热重载会重建部分 Widget 树,但平台的 service instance 可能没有同步重建。所以不要把ErrorOr实例直接塞进 InheritedWidget 或静态变量里。我踩坑之后的做法是,错误数据流统一走状态管理,比如使用 Cubit 或者 ChangeNotifier 持有 ErrorOr 状态。热重载时状态管理器会重建,但 ErrorOr 本身只是纯数据,重新获取一次最新数据就能恢复。
另外,鸿蒙平台上热重载的延迟比模拟器上更高,你看到错误提示可能不是实时的。如果要调试错误处理链路,别依赖热重载,老老实实用flutter run的冷启动。
5.3 错误堆栈在鸿蒙平台上被裁剪
Dart 在鸿蒙虚拟机上的堆栈信息,默认情况下会比标准 Flutter 平台少很多,尤其是涉及到 ErrorOr 的 flatMap 回调时,可能只剩下“Error or failure”而没有具体业务堆栈。这个问题在排查线上异常时非常致命。
我的做法是给 AppError 增加一个stackTrace字段,在创建错误时显式保留当前堆栈:
factory AppError.fromException(Object e, StackTrace st) { return AppError( code: 'UNKNOWN', message: e.toString(), stackTrace: st, ); }然后把 stackTrace 序列化到日志上报平台。虽然有些堆栈在鸿蒙上仍然不完整,但至少保留了 Dart 侧的错误触发点,排查时能少走很多弯路。
5.4 性能考量:不要滥用闭包
ErrorOr 的 flatMap 和 map 都接收闭包,每个闭包都会创建新的对象。如果你在列表中循环调用大量 ErrorOr 链,会产生额外的 GC 压力。鸿蒙设备的性能和安卓中端机差不多,我建议在密集计算场景里,直接用 switch 表达式代替过长的链式调用。
一个优化例子:
final result = ErrorOr.value(list) .flatMap((items) => items.map(_parseItem).toList()) .flatMap((parsed) => _saveToDb(parsed)) .onFailure((e) => _log(e));如果 list 有几百条数据,这个链上面会创建很多中间 ErrorOr 实例。更好的做法是,把 map 里需要逐项处理的逻辑先普通循环算完,最后再用 ErrorOr 包裹结果。error_or 的设计初衷是表达错误流,不是用来做集合变换的。
5.5 常见问题速查表
给一张表,方便直接查阅:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| pub get 卡住或报依赖冲突 | 鸿蒙版 Flutter SDK 的 Dart 版本旧 | 锁定 error_or 旧版本,或升级 Flutter SDK 分支 |
| 编译报 undefined class | 某个依赖是纯插件但未声明 ohos 实现 | 检查 pubspec hooks,改用纯 Dart 替代包 |
| 运行时报 MissingPluginException | 误把平台通道当纯逻辑使用 | 确认调用原生接口前,Native 侧已注册 |
| ErrorOr 热重载后丢失 | 错误状态被静态变量持有 | 改为使用 Cubit / Provider 管理状态 |
| 堆栈信息缺失 | 鸿蒙 VM 裁剪了 Dart 堆栈 | 显式保存 StackTrace 并上报 |
| 错误消息 UI 上乱码 | 错误码或消息包含特殊字符 | 统一在 AppError 中控制 message 格式 |
这些坑看着不大,但每一条都能耗掉半天时间。我做完这个适配后,最大的体会是:纯 Dart 三方库鸿蒙化,技术难度其实不高,真正的复杂度在于你要确保业务侧的使用方式严格遵守“错误即数据”的原则,同时在鸿蒙平台上把日志、上报、UI 反馈这些周边设施串起来。
最后分享一个小技巧:如果你要在鸿蒙 Flutter 工程里持续迭代 error_or 相关逻辑,建议在 ohos 目录外再维护一个纯 Dart 的测试工程,用来跑快速的单元测试。鸿蒙真机编译一次要一两分钟,单元测试秒级完成。我的日常工作流是先在纯 Dart 工程里把业务错误流的逻辑跑通,再同步到鸿蒙 Flutter 工程做真机验证。这样既保证了开发效率,又能在上真机前排除大部分低级错误。希望这套流程对正在做鸿蒙化改造的你能有些帮助。