☰
鸿蒙化适配指南:error_or库在Flutter流式错误处理中的应用
2026/10/2 14:38:19 网站建设 项目流程

写这篇鸿蒙化适配指南之前,先说说我为什么会对 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 SDKAPI 10 及以上,建议直接用 DevEco Studio 自带的 SDK
Flutter SDK for OpenHarmony建议用社区 3.7+ 的分支,跟随上游 Flutter 版本更新
Dart SDK跟随 Flutter SDK,无需单独安装
DevEco Studio4.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.yaml

ohos 目录相当于一个完整的鸿蒙原生壳工程,里面是 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 工程做真机验证。这样既保证了开发效率,又能在上真机前排除大部分低级错误。希望这套流程对正在做鸿蒙化改造的你能有些帮助。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询