☰
Flutter 状态管理框架对比(三):Riverpod 如何连起购物车与异步价格
2026/9/30 7:29:13 网站建设 项目流程

商品列表点了“加入购物车”,详情页的角标变了,结算页却还显示旧数量。把三个页面各放一个count,接下来就得追着页面同步;结算价再走一次接口,问题会更明显:数量和报价到底依赖哪份数据?

一句话结论:Riverpod 用 provider 保存共享状态、用ref.watch连起依赖,购物车变化会推动报价重新计算,而AsyncValue让结算页明确处理等待、结果和失败。

本文以flutter_riverpod: 3.4.3为准,使用手写 provider,不依赖代码生成。示例只演示本地购物车数量和模拟异步报价;真实项目仍需接入商品 ID、服务端报价、库存校验与提交订单流程。前一章的选型地图见系列第一章。

在pubspec.yaml中固定版本:

dependencies:flutter:sdk:flutterflutter_riverpod:3.4.3

这个依赖提供 Flutter 的ProviderScope与 Consumer Widget;下面的示例不使用生成器。

1. 先看这张依赖图

列表 / 详情 / 结算页 ──watch──> cartProvider(购物车数量) priceProvider(异步报价) ──watch──> cartProvider 结算页 ──watch──> priceProvider ──返回──> AsyncValue 按钮点击 ──read cartProvider.notifier──> add() 服务端价格变更通知 ──invalidate priceProvider──> 重新询价

ProviderScope持有 provider 的状态容器。顶层声明的 provider 是定义,不是一个随页面复制的全局可变值;同一个ProviderScope下,三个页面订阅同一份购物车状态。实际路由切换时,只要这些页面仍在该作用域中,读到的就是同一份状态。

这里的“依赖”有两个方向:界面观察状态,报价观察购物车。priceProvider的函数里写下ref.watch(cartProvider),数量变化就会让旧报价失效并重新求值,不必从列表页逐个通知结算页。

2.watch和read各放在哪里?

NotifierProvider负责可修改的购物车数量,Notifier.build()给出初始值,业务方法负责更新state。Widget 使用ref.watch(cartProvider)订阅数量;按钮回调使用ref.read(cartProvider.notifier).add()发起修改。不要用read代替界面中的watch来“省重建”,否则界面失去这条订阅关系。

下面是一段可改造的最小示例。它把三个页面的位置压成同一屏的三行,实际应用可把CartCount分别放进列表、详情和结算路由。延迟 300 毫秒和单价 19.9 元只为模拟异步过程,不是服务端实时报价。

import'package:flutter/material.dart';import'package:flutter_riverpod/flutter_riverpod.dart';finalcartProvider=NotifierProvider<Cart,int>(Cart.new);classCartextendsNotifier<int>{@overrideintbuild()=>0;voidadd()=>state=state+1;}finalpriceProvider=FutureProvider.autoDispose<double>((ref)async{finalcount=ref.watch(cartProvider);awaitFuture<void>.delayed(constDuration(milliseconds:300));returncount*19.9;// 演示数据:实际项目改为请求服务端报价});voidmain()=>runApp(constProviderScope(child:MaterialApp(home:CartDemo())),);classCartDemoextendsConsumerWidget{constCartDemo({super.key});@overrideWidgetbuild(BuildContextcontext,WidgetRefref)=>Scaffold(appBar:AppBar(title:constText('购物车演示')),body:Column(children:[for(finalplacein['商品列表','商品详情','结算页'])CartCount(place:place),ElevatedButton(onPressed:()=>ref.read(cartProvider.notifier).add(),child:constText('加入购物车'),),constCheckoutPrice(),],),);}classCartCountextendsConsumerWidget{constCartCount({super.key,requiredthis.place});finalStringplace;@overrideWidgetbuild(BuildContextcontext,WidgetRefref)=>Text('$place:${ref.watch(cartProvider)}件');}classCheckoutPriceextendsConsumerWidget{constCheckoutPrice({super.key});@overrideWidgetbuild(BuildContextcontext,WidgetRefref){finalquote=ref.watch(priceProvider);returnColumn(children:[quote.when(data:(total)=>Text('参考价:¥${total.toStringAsFixed(2)}'),loading:()=>constText('询价中…'),error:(error,stackTrace)=>Text('询价失败:$error'),),TextButton(onPressed:()=>ref.invalidate(priceProvider),child:constText('重新询价'),),],);}}

这段代码没有 HTTP、路由、持久化和错误文案映射,也没有处理报价与下单之间的价格变动。接入接口时,让服务端根据购物车明细返回报价;客户端的count × 19.9只能帮助看清状态流,不能作为结算依据。

3. 数量改变后,报价发生了什么?

初次构建时,CheckoutPrice观察priceProvider,得到的是AsyncValue<double>。它可能是加载中、已有数据或错误,when分别渲染这三种情况。点击按钮后,Cart.add()更新数量;三个CartCount收到变化,依赖购物车的priceProvider也重新执行。旧报价对应的那次计算不再是当前状态。

ref.invalidate(priceProvider)解决的是另一种变化:购物车数量没动,但服务端价格可能已经改了。失效会丢弃当前 provider 状态;若结算页仍在监听,就重新计算,若无人监听,就保持销毁,等下次读取再创建。重新加载期间,AsyncValue可能仍带着旧数据或错误,具体展示策略应由结算页决定;不应把旧报价当成可提交订单的最终价格。

Riverpod 3 默认对失败的 provider 自动重试。因此接入真实报价接口后,错误出现的时机和请求次数还会受重试策略影响。支付前要求一次确定结果的接口,应明确配置重试与超时策略,并让服务端在下单时再次确认价格。

4. 页面离开后,状态何时释放?

示例中的手写cartProvider没开启自动释放,适合让多个页面持续共享购物车。priceProvider使用.autoDispose:没有监听者后,Riverpod 会等待一帧;仍无人监听时释放它。再次进入结算页会重新求价。对于报价这种容易过期的数据,我倾向先用自动释放,再按业务允许的缓存时间调整。

有两条边界容易混淆。第一,依赖重算也会销毁旧状态,即使配置了keepAlive,旧报价也不会因为“保活”而绕过购物车变化。第二,代码生成的 provider 默认自动释放,可用@Riverpod(keepAlive: true)关闭;本文的手写 provider 则显式写.autoDispose。需要在一次成功请求后临时保留结果时,可以在自动释放的 provider 内调用ref.keepAlive(),但这也不能代替报价有效期和服务端校验。

如果报价请求支持取消,把取消动作注册到ref.onDispose。Riverpod 3 在 provider 销毁后继续使用它的ref会抛出异常;跨await后还要访问ref,先检查ref.mounted。本文的模拟请求在await后只使用之前捕获的count,因此没有再次读取ref。

5. 这套写法适合放在哪里?

列表、详情、结算共同依赖购物车时,让Notifier作为修改入口,页面只订阅自己需要的值,比在路由之间传三份数量更容易查清更新路径。异步报价由单独的FutureProvider承担,出错时也不会把购物车数量一起抹掉。输入框焦点、当前展开的卡片之类只影响单页的状态,仍可留在 Widget 内。

迁移旧示例时要留意版本:Riverpod 3 将StateProvider、StateNotifierProvider和ChangeNotifierProvider归入legacy.dart,本文使用当前主 API 中的NotifierProvider。这一章最值得记住的路径是:按钮调用修改方法,购物车状态更新,依赖它的报价重算,结算页从AsyncValue决定显示什么。

参考资料

  • flutter_riverpod 3.4.3(pub.dev)
  • Riverpod:Providers
  • Riverpod:Refs
  • Riverpod:Consumers
  • Riverpod:Automatic disposal
  • Riverpod:Migrating from 2.0 to 3.0

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

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

立即咨询