You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 17:42:21