Flutter中FutureProvider与StreamProvider联合使用的报错与优化
问题描述
我有两个数据模型:
- Product
- Jobs
在main.dart的_MyAppState(顶层Widget)中通过StreamProvider读取Firestore集合:
MultiProvider( providers: [ StreamProvider<List<Job>?>( create: (_) => JobsProvider.readJobs(), initialData: [], catchError: (_, __) => null, ) ], )
根据用户角色跳转的页面中,在build函数里创建了FutureProvider:
MultiProvider( providers: [ FutureProvider<List<Product>>( create: (_) => fetchProductsFromBase(), initialData: []), //.. 其他providers ], )
现在实现了一个StatefulWidget的State类,需要结合两个Provider中Firestore的特定字段(Job的int类型qty、Product的double类型dim_x和dim_y)展示结果:
List<Product>? productList; @override Widget build(BuildContext context) { productList = context.watch<List<Product>>(); print('NUM PRODUCTS = ${productList?.length}\n'); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Delivery Schedule', style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.black87)), const SizedBox( height: 15.0, ), Selector<List<JobDetailWithID>, List<Map<String, dynamic>>>( selector: (BuildContext context, List<JobDetailWithID> jobs) => getDeliveriesByDeliveryDate(jobs, numDaysSchedule, productList ?? []), builder: (BuildContext context, List<Map<String, dynamic>> toBeDeliveredJobs, _) { return Wrap( // ... 自定义UI,使用toBeDeliveredJobs ); }, ), ], ); } List<Map<String, dynamic>> getDeliveriesByDeliveryDate( List<JobDetailWithID> jobs, int numDaysSchedule, List<Product> productList) { List<Map<String, dynamic>> deliveries = []; // ... 计算逻辑,将结果添加到deliveries // ... 包含如下代码片段 // areaForTheDay = { // 'delivery_date': date.add(Duration(days: i)), // 'target': productList.length == 0 // ? 'NA' // : GetSomeDoubleValueFromJobs() return deliveries; }
当前问题
打印语句显示初始productList长度为0,导致Selector抛出_TypeError:type 'String' is not a subtype of type 'num'。
需要解决该错误,确保Widget在获取到所有Provider的数据后再构建,同时确认是否正确使用上述三个特定字段以减少Firebase读取次数。
编辑补充:已实现一种等待数据就绪后再构建的方案,寻求更优的性能优化方案。
解决方案
1. 直接修复类型不匹配错误
错误根源是target字段在productList为空时返回字符串'NA',但UI组件可能期望该字段是数值类型。可二选一调整:
- 统一类型:将空状态的
'NA'改为数值占位符(如0或null),在UI层处理显示逻辑:'target': productList.isEmpty ? 0 : GetSomeDoubleValueFromJobs() - 类型适配:保持
target为dynamic类型,在Widget构建时判断类型再渲染:// 在builder的UI代码中 Text(toBeDeliveredJobs[index]['target'] is num ? toBeDeliveredJobs[index]['target'].toString() : 'NA')
2. 等待所有数据就绪再构建Widget
方案1:双数据源状态判断
在build函数中同时监听两个Provider的数据,任一数据源未就绪时显示加载状态:
@override Widget build(BuildContext context) { final jobs = context.watch<List<JobDetailWithID>?>(); final productList = context.watch<List<Product>>(); // 自定义就绪条件:jobs不为null且productList非空 if (jobs == null || productList.isEmpty) { return const Center(child: CircularProgressIndicator()); } return Column( // ... 原有UI代码 children: [ // ... Selector<List<JobDetailWithID>, List<Map<String, dynamic>>>( selector: (_, jobs) => getDeliveriesByDeliveryDate(jobs, numDaysSchedule, productList), // ... ), ], ); }
方案2:复合Provider预计算结果
在页面层级创建复合Provider,等待两个数据源就绪后直接输出计算结果,避免重复计算:
// 在页面的MultiProvider中添加 FutureProvider<List<Map<String, dynamic>>>( create: (context) async { // 等待Product数据加载完成 final productList = await context.read<FutureProvider<List<Product>>>.future; // 监听Job流的第一个有效数据 final jobs = await context.read<StreamProvider<List<JobDetailWithID>>>.first; return getDeliveriesByDeliveryDate(jobs, numDaysSchedule, productList); }, initialData: [], )
之后在Widget中直接监听该复合Provider:
final deliveries = context.watch<List<Map<String, dynamic>>>(); if (deliveries.isEmpty && /* 判断是否为初始未加载状态 */) { return const CircularProgressIndicator(); } // 直接使用deliveries构建UI
3. Firebase读取性能优化
字段按需读取
如果当前查询是读取集合所有字段,会造成带宽浪费。修改Firestore查询,只获取需要的字段:
- Job集合仅读取
qty及关联字段:// JobsProvider.readJobs()中的查询 FirebaseFirestore.instance .collection('jobs') .select(['qty', 'delivery_date', /* 其他必要字段 */]) .snapshots() .map((snap) => snap.docs.map((doc) => Job.fromSnapshot(doc)).toList()); - Product集合仅读取
dim_x、dim_y及关联ID:// fetchProductsFromBase()中的查询 FirebaseFirestore.instance .collection('products') .select(['dim_x', 'dim_y', 'id']) .get() .then((snap) => snap.docs.map((doc) => Product.fromSnapshot(doc)).toList());
Provider复用与缓存
- 页面级
FutureProvider可设置autoDispose并开启keepAlive: true,避免重复请求Firestore:FutureProvider<List<Product>>.autoDispose( create: (_) => fetchProductsFromBase(), initialData: [], keepAlive: true, ) - 优化
Selector的selector函数,仅传递所需字段减少重计算:
同时修改selector: (context, jobs) { // 仅提取Product中需要的dim_x、dim_y映射 final productDims = productList.map((p) => {'id': p.id, 'dim_x': p.dim_x, 'dim_y': p.dim_y}).toList(); return getDeliveriesByDeliveryDate(jobs, numDaysSchedule, productDims); },getDeliveriesByDeliveryDate的参数类型为List<Map<String, dynamic>>,仅处理必要字段。
内容的提问来源于stack exchange,提问作者pbs
相关产品推荐
相关产品推荐

