ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenHarmony上Flutter状态管理选型:Riverpod与Bloc实战深度对比

OpenHarmony上Flutter状态管理选型:Riverpod与Bloc实战深度对比 1. 为什么这个话题在OpenHarmony上特别值得纠结这段时间我在折腾 OpenHarmony 设备上的 Flutter 应用团队群里几乎每天都会聊到同一个问题Riverpod 和 Bloc 到底选哪个。说实话如果只是普通 Android 项目这个问题已经被聊烂了网上随便一搜都是对比文章闭着眼睛选一个都能跑。但放在 OpenHarmony 上情况完全不是一回事。Flutter 在 OpenHarmony 上的运行依赖社区的引擎适配版本构建方式、插件打包、生命周期处理都和标准 Flutter 工程有差异状态管理方案选错后面填坑的时间远超写代码的时间。这篇文章不是要告诉你选 A 就对了而是把我在 OpenHarmony 设备上实际跑 Flutter 应用时对 Riverpod 和 Bloc 的底层逻辑、接入方式、性能表现、报错场景做过的一次完整评估记录下来。适合正在做 OpenHarmony 应用适配、或者准备在 OHOS 设备上启动 Flutter 新项目的团队参考。即使你目前只在标准 Flutter 上开发看完也能对这两个方案有更清晰的理解至少知道以后遇到状态管理选型时应该从哪些维度去思考。1.1 别拿 Android 的选型经验硬套先说一个最容易踩的坑。Flutter 官方框架在不同平台上的能力边界并不一样OpenHarmony 上的 Flutter 适配层由社区维护和标准 Flutter 之间存在不少差异渠道包发布用的不是 APK/AAB 而是 HAP原生侧构建走的是 hvigor 工程Dart 层的第三方包虽然还是通过 pubspec 管理但插件如果涉及原生代码就需要把原生部分打包成 AAR 或 HAR 再集成。这个差异直接决定了状态管理选型时要多考虑一个因素你选的方案能不能在 OpenHarmony 的工程形态下稳定跑起来。另一个问题是平台通道的稳定性。Flutter 应用在 OpenHarmony 上调用系统能力比如相机、传感器、平台视图都要通过 PlatformChannel 或原生插件桥接。一旦插件在底层抛异常错误日志经常是一大堆原生堆栈加 Dart 层的 Unhandled Exception而状态管理库本身又是一个相对复杂的异步体系出问题时很难判断是状态管理导致的还是平台通道导致的。我在排查过程中发现Riverpod 和 Bloc 对错误处理的姿势完全不同这会直接影响你在 OpenHarmony 上排查问题的效率。所以真正值得评估的不是哪个方案更流行而是在 OpenHarmony 这个特定平台上哪个方案的收益更大、风险更可控。我这次评估的场景包括全局登录态、异步数据拉取、表单校验、相机权限、页面间通信、低端设备帧率表现。下面先把我对两个方案的核心理解拆开再讲实操。1.2 我设定了一个什么样的对比边界为了避免空对空讨论我先明确这次对比的边界条件平台OpenHarmony 设备Flutter 使用社区适配引擎版本构建产物需要过 XTS 兼容性测试。团队3-5 人的小团队成员 Flutter 经验参差不齐有 Android 原生背景。业务工具类应用页面数量大概 20-30 个存在实时数据刷新、后台任务、第三方登录等场景。评判维度学习成本、样板代码量、测试容易度、错误可追踪性、包体积影响、在 OpenHarmony 上的实测性能表现。有了边界之后很多讨论就不用再纠结复杂度高的大项目该用谁这种玄学问题了。小团队写应用代码可读性和排查效率往往比技术上限更重要。2. 先拆设计哲学Riverpod 和 Bloc 各自怎么看待状态这件事选型之前必须先把底层机制搞清楚。很多对比文章只停留在 API 层面比如Riverpod 不用 BuildContext、Bloc 要写很多 Event但真正决定体验的是两个库对状态变化传播、依赖管理、异步调度这三个核心问题的处理方式。2.1 Riverpod以监听为中心的响应式依赖图Riverpod 的核心思路是建立一个 Provider 依赖图。每个 Provider 都可以被其他 Provider 监听当上游数据变化时下游的 Provider 和 UI 会自动收到通知并重建。最关键的 API 是ref.watch它不仅是读取数据更是注册依赖。只要你在某个 Widget 的 build 方法里对某 Provider 调用了ref.watch这就相当于告诉 Riverpod这个 Widget 的渲染结果依赖这个数据数据一变Widget 就要重新 build。这个机制的底层是非常典型的响应式依赖追踪。我们可以把 Provider 之间通过ref.watch建立的关系看成一张有向图任何一个节点发生变化Riverpod 会沿着依赖边逐级通知下游并且保证每个节点只重建一次不会出现重复计算。对于页面上的局部状态你可以用一个很小的 Provider 包起来谁用谁 watch完全不经过全局事件总线数据流非常清晰。Riverpod 还有一层比较隐藏的优势就是和 Dart 的异步事件循环结合得很自然。它内部处理异步数据时使用AsyncValue来统一表示 loading、data、error 三种状态。你不需要自己维护请求中、成功、失败的枚举也不用担心异步回调里 setState 导致 Widget 已销毁的经典崩溃。因为 Provider 销毁时Riverpod 会主动取消对应监听关系。不过Riverpod 的学习曲线并不是不用 BuildContext 就更简单。它引入了 ProviderScope、ConsumerWidget、ref.listen、ref.read、AsyncValue.when 等一系列概念新手刚开始很容易困惑什么时候用ref.read什么时候用ref.watchref.listen和ref.watch有什么区别这些细节决定了你写出来的代码是看起来很高级还是真的高性能。2.2 Bloc把状态变化当成业务流程来管理Bloc 的设计哲学完全走另一条路线。它本质上是一个事件驱动的状态机外部通过add(Event)提交业务事件Bloc 内部接收事件后执行异步逻辑比如请求网络、读写缓存最终通过emit(State)发出一个新的状态UI 通过监听状态变化来刷新。整个过程是单向数据流Event 进去State 出来不会存在多个 Provider 互相 watch 导致的循环依赖问题状态变化路径是线性的、可追踪的。这带来的最大好处是业务逻辑的可预测性。以登录功能为例用户在页面上点击登录按钮你只需要add(LoginRequested(phone, code))登录接口的调用、loading 状态的切换、错误信息的映射全部发生在 Bloc 内部。UI 层既不关心接口怎么调用也不会直接操作异步结果只需要根据不同的 AuthState 渲染不同界面。当出现 bug 时通过 event log 能几乎精确还原每一步状态变化。Bloc 内部对事件的处理是异步队列模型。默认情况下多个事件会逐个处理后一个事件必须等前一个事件处理完才会被取出执行这保证了状态变化的顺序不会乱。但如果你在一个事件处理函数里发起了比较耗时的异步操作然后又连续 add 了好几个事件就会形成排队某些场景下需要手动处理并发和取消策略不能简单把事件往队列里怼。和 Riverpod 的可视化依赖图相比Bloc 更像一套严格的项目管理流程。它试图用统一的事件流规范约束团队里的每个人让代码风格保持一致。代价就是样板代码非常多一个简单的 Counter 都要拆成 Event、State、Bloc 三个文件。2.3 性能与调度哪个方案在 OpenHarmony 上更稳理解了设计哲学之后性能差异也就能说清楚了。我在测试中发现Riverpod 在普通页面上的重建粒度确实比 Bloc 更细。因为ref.watch可以被声明在任意粒度你可以只 watch 某个 Provider 里的一个字段数据变化时只有这个控件重建兄弟控件完全不受影响。Bloc 则是以 State 为单位通知一个emit出去所有监听这个 Bloc 的 BlocBuilder 都会收到通知如果你不配合buildWhen或者BlocSelector做局部过滤容易造成整棵树大范围重建。但这不意味着 Bloc 性能就差。如果一个页面的状态数量本身就很少比如只有 loading 和 data 两种那 Bloc 的整包通知和 Riverpod 的细粒度重建在表现上几乎没有区别。真正的差异在高频变化场景比如相机预览时的参数调整、实时传感器数据刷新如果状态每秒变化几十次Riverpod 的细粒度依赖追踪和 Bloc 的与buildWhen结合都能扛住重点在于你怎么写。整体来看两个库性能都能满足 OpenHarmony 上的常规业务真正影响体验的是异步调度。这里要提到 Dart 事件循环的一个细节。很多人问 Future 的 then 回调是不是放进微任务队列答案是肯定的。Flutter 的单线程模型下微任务会在当前事件处理完成后、下一个事件开始前全部执行。如果你的状态管理方案在一个状态变更后连续触发了多个异步回调每个回调又通过ref.watch或emit触发新的重建这些微任务会排队抢占 UI 线程时间。在 OpenHarmony 的低端设备上这种微任务风暴可能导致掉帧。我实测中Riverpod 的依赖图更新和 Bloc 的事件队列在短时间内大量触发时都会出现类似现象但 Riverpod 因为依赖图自动去重的机制微任务数量通常更少。2.4 生活化类比电子表格 vs 业务流程系统用生活化的方式理解这两个库Riverpod 更像一张自动更新的电子表格。你在 A 格子输入用户已登录所有引用了 A 格子的公式都会自动重新计算你不需要手动告诉每个格子你该更新了因为依赖关系是声明出来的。Bloc 则更像一套严格的业务流程系统。每次操作都要填单Event交给后台处理处理完印章State前台收到盖章后的单据再更新页面。它的优点是每个版本都有记录、有审批流适合多人协作的大部门缺点是做一件小事也要走完整流程。这个类比直接解释了为什么 Riverpod 更适合快速迭代的小项目而 Bloc 更适合需要多人协作、长期维护的大型项目。但这只是通用判断OpenHarmony 上的实际情况要结合在一起看。3. 在 OpenHarmony 工程里实操同一场景两套实现讲完理论我直接用同一个业务场景在 OpenHarmony 工程里分别用两套方案实现了一遍。场景很简单登录态管理 相机权限申请与预览。为什么选这个场景因为登录态是全局状态相机权限是异步状态两者都涉及生命周期和异常处理基本能覆盖大部分状态管理需求。3.1 接入前的三个检查点在 OpenHarmony 工程里接入之前我建议先检查三件事。第一确认 Flutter 引擎适配版本。OpenHarmony 上的 Flutter 引擎版本落后于标准 Flutter所以不要一上来就使用最新版本的状态管理库要以你当前 Flutter 版本支持的最低 Dart SDK 为准。比如某些 Riverpod 新版本要求 Dart 3如果你的适配引擎还在 Dart 2.x就需要锁定兼容的旧版本。第二理解插件的打包方式。Dart 层通过 pubspec 引入依赖没有任何问题但如果状态管理方案需要配合原生插件使用那个插件要能打包成 OpenHarmony 原生模块可集成的 AAR 或 HAR。建议在 pubspec 里引入某个包之前先去它的仓库看有没有 OpenHarmony 适配分支或 issue 记录。第三工程构建不要混用 Gradle 和 hvigor。OpenHarmony 应用工程用 DevEco Studio 和 hvigor 构建但如果你是从标准 Flutter 工程迁移过来保留了 Android 壳工程很容易在 settings.gradle 里看到这样一行apply plugin: dev.flutter.flutter-gradle-plugin这就是典型的命令式 apply 用法新版 Flutter 会警告你改用 Plugin DSL。在 OpenHarmony 混合工程里这种写法更容易引人困惑。最佳实践是直接用 OpenHarmony 工程的模板结构把 Flutter 依赖打包成 AAR/HAR 交给 hvigor 引用不要让 Gradle 参与 OpenHarmony 侧构建。3.2 Riverpod 实现登录态 相机权限Riverpod 这套我选择了 AsyncNotifier 来承载异步逻辑。关键代码结构如下// auth_provider.dart class AuthNotifier extends AsyncNotifierAuthState { override FutureAuthState build() async { // 从本地缓存恢复登录态 final cached await authLocalSource.load(); return cached ?? const AuthState.unauthenticated(); } Futurevoid login(String phone, String code) async { state const AsyncLoading(); state await AsyncValue.guard(() async { final user await authRepository.login(phone, code); await authLocalSource.save(user); return AuthState.authenticated(user); }); } Futurevoid logout() async { await authLocalSource.clear(); state const AsyncData(AuthState.unauthenticated()); } } final authProvider AsyncNotifierProviderAuthNotifier, AuthState(AuthNotifier.new);为什么要用 AsyncNotifier 而不是普通 Notifier因为build()方法本身有异步恢复流程AsyncNotifier 内置了 loading 状态UI 可以直接通过state.when来分情况渲染省去自己定义正在恢复登录态的标志位。相机权限的状态我单独用一个小的StateProvider管理没有混进登录态 Provider避免权限变化触发登录相关 UI 重建final cameraPermissionProvider StateProviderCameraPermissionStatus((ref) CameraPermissionStatus.unknown);在页面中这样使用class CameraPage extends ConsumerWidget { override Widget build(BuildContext context, WidgetRef ref) { final authState ref.watch(authProvider); final permission ref.watch(cameraPermissionProvider); // 认证状态与权限状态分开监听变化时各刷各的 } }实测下来最舒服的一点是页面销毁时不需要手动取消异步请求。因为 Riverpod 的 Provider 生命周期和 ProviderScope 绑定页面退出后如果 Provider 没有再被 watch会在合适时机自动销毁。在 OpenHarmony 设备上相机页面频繁进入退出时没有出现过异步回调操作已销毁 Widget的崩溃。3.3 Bloc 实现同样的功能Bloc 这套代码量明显更多。先定义事件和状态// auth_state.dart sealed class AuthState { const AuthState(); } class AuthInitial extends AuthState { const AuthInitial(); } class AuthLoading extends AuthState { const AuthLoading(); } class AuthAuthenticated extends AuthState { final User user; const AuthAuthenticated(this.user); } class AuthUnauthenticated extends AuthState { const AuthUnauthenticated(); }// auth_event.dart sealed class AuthEvent { const AuthEvent(); } class AuthLoginRequested extends AuthEvent { final String phone; final String code; const AuthLoginRequested(this.phone, this.code); } class AuthLogoutRequested extends AuthEvent { const AuthLogoutRequested(); }然后才是 Bloc 主体// auth_bloc.dart class AuthBloc extends BlocAuthEvent, AuthState { AuthBloc({required AuthRepository repository}) : _repository repository, super(const AuthInitial()) { onAuthLoginRequested(_onLoginRequested); onAuthLogoutRequested(_onLogoutRequested); } final AuthRepository _repository; Futurevoid _onLoginRequested( AuthLoginRequested event, EmitterAuthState emit) async { emit(const AuthLoading()); try { final user await _repository.login(event.phone, event.code); emit(AuthAuthenticated(user)); } catch (e) { emit(AuthUnauthenticated()); } } Futurevoid _onLogoutRequested( AuthLogoutRequested event, EmitterAuthState emit) async { await _repository.clearLocal(); emit(const AuthUnauthenticated()); } }页面端用 BlocProvider 注入使用 BlocBuilder 监听class AuthPage extends StatelessWidget { override Widget build(BuildContext context) { return BlocProvider( create: (_) AuthBloc(repository: context.read()), child: const AuthView(), ); } } class AuthView extends StatelessWidget { override Widget build(BuildContext context) { final authState context.watchAuthBloc().state; return switch (authState) { AuthInitial() const SizedBox.shrink(), AuthLoading() const CircularProgressIndicator(), AuthAuthenticated(:final user) Text(欢迎: ${user.name}), AuthUnauthenticated() const LoginButton(), }; } }Bloc 的样板代码确实多但它的好处也很直接所有状态变化的入口都集中在 Event 上。你不需要在多个页面里各自调用 repository 方法页面只要知道我能提交哪些事件至于登录失败后是跳转错误页还是弹 toast完全由 Bloc 内部决定。在 OpenHarmony 这种三端UI、原生插件、Dart 引擎协同调试的环境里这个特性非常宝贵——因为你只需要在一个文件里找逻辑不用追踪十几个 Provider。3.4 两版代码在 OHOS 上的生命周期注意点OpenHarmony 页面的生命周期和标准 Flutter 大体一致但请注意一个隐藏差异部分 OpenHarmony 设备在应用切到后台后Flutter 引擎会进入低功耗模式帧调度会暂停。如果你在状态管理里监听了生命周期变化注意不要用WidgetsBindingObserver直接去监听系统级事件尽量通过 OpenHarmony 侧的生命周期回调通知到 Dart 层。对于 Riverpod重点看 Provider 的自动销毁是否按预期触发。如果一个全局 Provider 需要在应用冷启动后一直存活记得把 ProviderScope 放在整个应用的根部。对于 BlocBlocProvider会自动管理 Bloc 的关闭但如果你在页面外创建 Bloc比如用BlocProvider.value传一个到处共享的实例那这个 Bloc 的close()就要你自己负责。我遇到过的情况是页面销毁后 Bloc 里还挂着一个相机预览的异步任务原生侧的相机资源没有释放导致再次进入页面时黑屏。后来我把相机订阅的取消放在了close()和页面 dispose 两处问题才解决。注意在 OpenHarmony 上Flutter 页面销毁并不等于原生页面销毁。如果你的状态管理持有相机、传感器这类原生资源必须在两个层面都做清理Dart 层停止异步逻辑原生层释放资源。两边同时处理才保险。4. 跑起来以后我遇到的 OpenHarmony 特有坑理论对比再充分也抵不过真机跑一圈。下面这些坑是我在 OpenHarmony 设备上实测时遇到的有些和状态管理本身无关但因为状态管理库的存在它们会被放得很大。4.1 一张频出的错误日志dart_vm_initializer.cc(41) 未处理异常OpenHarmony 上最常见的崩溃日志长这样e/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: ...这行日志很多人第一眼会以为是 Dart VM 初始化失败其实它只是说 Dart 层有一个未捕获的异常位置在 dart_vm_initializer 这个入口文件里。真实异常信息一般在下几行比如LateInitializationError、SocketException或者某个 Provider 的 build 抛错。排查这类问题我的建议是两步走。第一步在runApp外层包一个ErrorWidget.builder让 UI 层能看到具体错误。第二步在访问可能为空的资源前加防御比如 Riverpod 里用state.when而不是直接state.valueBloc 里用switch表达式声明所有状态分支避免未匹配状态。在 OpenHarmony 上排查效率最高的工具其实是日志关键字过滤先搜Unhandled Exception再往上翻 200 行看最后一个业务日志大部分时候能直接定位到是哪个 Provider 或 Bloc 抛的异常。4.2 Flutter AAR/HAR 打包与 Gradle 插件问题如果你维护过一个包含原生功能的 Flutter 插件想在 OpenHarmony 工程里复用大概率会遇到插件怎么打包的问题。标准 Flutter Android 插件是 AAR 产物但 OpenHarmony 应用需要使用 HAP 集成原生模块这时候要么找社区的 OpenHarmony 适配包要么把原生代码改造成 HAR 提供给 hvigor 引用。如果你的工程是从旧工程迁移来的还会看到这种报错You are applying Flutters main Gradle plugin imperatively using the apply script method这不是致命错误但说明你的 settings.gradle 还在用旧式 apply。改法很简单在 plugins 块里声明plugins { id dev.flutter.flutter-gradle-plugin id org.jetbrains.kotlin.android }去掉原来的apply plugin行。放在 OpenHarmony 工程语境下重点是别把 Android 侧构建流程和 OpenHarmony 的 hvigor 构建流程混在一起。状态管理在 Dart 层是纯逻辑跟打包方式没关系但一旦你为了打包插件去改构建脚本改动范围扩大后很容易引入莫名其妙的依赖冲突。4.3 PlatformView 与 Camera 场景下的状态管理坑OpenHarmony 上的 Flutter 对 PlatformView 的支持和标准 Android 存在差异尤其在相机预览这类需要原生 Surface 接管渲染的场景里。你会发现页面布局里有一块区域是原生相机画面Flutter 控件浮在上面。此时状态管理里的权限状态和页面可见状态很容易错位。我遇到的现象是从相机页返回时权限 Provider 已经变为 granted但平台视图还没有释放完毕状态管理触发了新 UI 的 build导致在原生 Surface 上绘制 Flutter 控件产生黑屏或者闪烁。排查后确认是状态与资源生命周期不同步。解决方案是不要直接在状态里保存相机已开启这种复合信息而是拆成权限是否授予和页面是否可见两个独立状态只有两者都为 true 时才去初始化相机预览。4.4 XTS 兼容性测试前的性能自查如果你需要做 OpenHarmony 的 XTS 认证状态管理方案会影响三个指标冷启动时间、内存占用、长时间运行后的稳定性。Bloc 因为初始化时要创建多个 Event/State 对象冷启动时内存占用略高Riverpod 的依赖图是惰性初始化的在页面级 Provider 大量存在时反而更省内存。不过这些都是相对差异真正决定能不能过认证的是你是否在高频路径上做了不必要的重建。我在自查时用了一个笨办法在每个页面 build 入口打时间戳观察页面滑动时每秒重建次数。结果发现用 Bloc 时如果不加buildWhen列表页会频繁重建整个 bodyRiverpod 如果滥用ref.watch的父级 Provider也会导致同页面的多个部分联动。所以性能优化不看方案看使用方式。5. 那到底怎么选我最后的决策逻辑最后聊聊我的决策逻辑以及一个实际用过的建议。5.1 一张决策速查表我把关键对比项整理成了表格方便你在团队讨论时直接拿出来当参考维度RiverpodBloc学习成本中等概念多但语法简单偏高Event/State/Bloc 三层要理解透彻样板代码量少一个 Provider 声明即可多一个功能要拆多个文件异步支持AsyncValue 内置 loading/error/data手动在 State 里建模错误处理靠 try/catch依赖关系可视化依赖图自动去重更新事件流线性可追踪测试容易度ProviderContainer 可直接读状态BlocTest 支持事件行为驱动但初始化更繁琐OpenHarmony 适配风险纯 Dart 层逻辑风险主要看版本兼容同上但关闭时机要手动管理适合团队3-5 人快速迭代需要强约束的多团队协作适合业务中后台、工具类、状态分散强流程、事件驱动明确、需要审计5.2 我的最终选择与真实心得如果现在让我在 OpenHarmony 上选我会按这个原则新项目、小团队、快速验证的业务优先 Riverpod已经有严格代码规范、多人协作的大团队优先 Bloc。我自己最后在 OpenHarmony 项目里用了 Riverpod核心原因是排查效率。在 OpenHarmony 上调试已经比标准 Flutter 多了一层复杂度Bloc 的样板代码字面上看没问题实际排查时要跨文件跳转反而增加心智负担。最后分享一个我踩过几次坑之后养成的习惯把状态逻辑和 UI 层彻底分离单独建一个纯 Dart 包维护 Provider 或 Bloc。这样万一将来要从一个方案切到另一个方案只需要重写这个纯 Dart 包UI 层改动可以压到最小。在 OpenHarmony 这种构建链路比较长的平台上任何一次大范围代码重构的成本都会比标准 Flutter 翻倍提前做好这层隔离比选哪个状态管理库更重要。
返回列表