Flutter生成PDF上传Firebase Storage:MacOS正常Web端报错
Flutter Web端PDF上传Firebase Storage报错问题解决
我正在为公司开发一款基于Flutter的报价单生成应用,应用可根据用户选择生成PDF文档并存储到Firebase Storage。功能在MacOS平台运行完全正常,但在Flutter Web端运行时,尝试将生成的PDF文件存储到Firebase Storage时抛出completer.complete错误,错误信息不足以定位根因。
问题核心原因
- Web端不支持
dart:io的File类:Web环境没有本地文件系统访问权限,getApplicationDocumentsDirectory和File操作在Web上无法正常工作,这是导致报错的主要原因。 - 重复初始化Firebase:
generateQuotationPDF函数中再次调用await Firebase.initializeApp(),但应用入口main函数已经完成初始化,重复初始化会引发异常。 - 上传方式不兼容Web:使用
putFile方法上传文件,Web端应该直接使用字节数据上传,而非本地文件对象。
解决方案
针对Web端的特性,修改PDF生成和上传逻辑,移除IO相关依赖,直接操作字节数据:
修改后的pdf_generator.dart代码
import 'package:flutter/services.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:shared_preferences/shared_preferences.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:firebase_core/firebase_core.dart'; // 获取当前报价单号 Future<int> _getQuotationNumber() async { SharedPreferences prefs = await SharedPreferences.getInstance(); int quotationNumber = prefs.getInt('quotation_number') ?? 0; return quotationNumber; } // 递增报价单号 Future<void> _incrementQuotationNumber() async { SharedPreferences prefs = await SharedPreferences.getInstance(); int quotationNumber = await _getQuotationNumber(); quotationNumber++; prefs.setInt('quotation_number', quotationNumber); } Future<String> generateQuotationPDF( Map<String, String> selectedItemNamesWithSKU, Map<String, int> quantities, Map<String, double> sellingPrices, Map<String, String> itemSku, Map<String, double> itemCostPrices, Map<String, double> itemPrices, double totalCost, double totalSellingPrice, double discount, ) async { final pdf = pw.Document(); // 递增报价单号 await _incrementQuotationNumber(); int quotationNumber = await _getQuotationNumber(); String fileName = 'quotation_pdfs/PC-${quotationNumber.toString().padLeft(3, '0')}.pdf'; // 加载Logo图片 final logoImage = pw.MemoryImage( (await rootBundle.load('assets/cazalogo.webp')).buffer.asUint8List(), ); // 获取当前日期并格式化 final currentDate = DateTime.now(); final formattedDate = "${currentDate.year}-${currentDate.month.toString().padLeft(2, '0')}-${currentDate.day.toString().padLeft(2, '0')}"; // 添加PDF内容 pdf.addPage( pw.Page( build: (pw.Context context) { final List<List<String?>> tableData = [ ['Item', 'SKU', 'Qty', 'Cost', 'Selling Price'], // 表头 for (String type in selectedItemNamesWithSKU.keys) if (selectedItemNamesWithSKU[type] != null && selectedItemNamesWithSKU[type]!.isNotEmpty && quantities[selectedItemNamesWithSKU[type]] != 0) [ selectedItemNamesWithSKU[type], itemSku[selectedItemNamesWithSKU[type]] ?? '', quantities[selectedItemNamesWithSKU[type]]?.toString() ?? '', '${(itemCostPrices[selectedItemNamesWithSKU[type]] ?? 0.0) * (quantities[selectedItemNamesWithSKU[type]] ?? 0)} BHD', '${(itemPrices[selectedItemNamesWithSKU[type]] ?? 0.0) * (quantities[selectedItemNamesWithSKU[type]] ?? 0)} BHD', ], ]; return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Center(child: pw.Image(logoImage, width: 50, height: 50)), pw.Text('Quotation #PC-${quotationNumber}', style: const pw.TextStyle(fontSize: 20)), pw.Divider(), pw.SizedBox(height: 6), pw.Text('Cazasouq Trading W.L.L', style: const pw.TextStyle(fontSize: 10)), pw.Text('Cazasouq Shop 983d Block 332 Road 3221 Bu ashira', style: const pw.TextStyle(fontSize: 10)), pw.Text(formattedDate, style: const pw.TextStyle(fontSize: 10)), pw.SizedBox(height: 20), pw.Text('Quotation Details', style: const pw.TextStyle(fontSize: 15)), pw.Divider(), pw.SizedBox(height: 10), // ignore: deprecated_member_use pw.Table.fromTextArray( headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 10), cellStyle: const pw.TextStyle(fontSize: 7), border: pw.TableBorder.all(), headerDecoration: const pw.BoxDecoration(color: PdfColors.grey300), cellHeight: 25, cellAlignments: { 0: pw.Alignment.centerLeft, 1: pw.Alignment.center, 2: pw.Alignment.center, 3: pw.Alignment.center, 4: pw.Alignment.center, }, headerHeight: 20, headerPadding: const pw.EdgeInsets.all(5), cellPadding: const pw.EdgeInsets.all(5), data: tableData, columnWidths: { 0: const pw.FlexColumnWidth(5), 1: const pw.FlexColumnWidth(2), 2: const pw.FlexColumnWidth(1), 3: const pw.FlexColumnWidth(2), 4: const pw.FlexColumnWidth(2), }, ), pw.SizedBox(height: 10), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('Total Cost: $totalCost BHD', style: const pw.TextStyle(fontSize: 9)), pw.Text('Total Selling Price: $totalSellingPrice BHD', style: const pw.TextStyle(fontSize: 9)), ], ), pw.SizedBox(height: 5), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('Discount: $discount BHD', style: const pw.TextStyle(fontSize: 9)), pw.Text('Selling Price After Discount: ${totalSellingPrice - discount} BHD', style: const pw.TextStyle(fontSize: 9)), ], ), pw.SizedBox(height: 5), pw.Row( mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ pw.Text('Margin: ${totalSellingPrice - totalCost} BHD', style: const pw.TextStyle(fontSize: 9)), pw.Text('Margin After Discount: ${totalSellingPrice - totalCost - discount} BHD', style: const pw.TextStyle(fontSize: 9)), ], ), pw.SizedBox(height: 5), pw.Text('Selling Price After Discount + VAT: ${(totalSellingPrice - discount) * 1.1} BHD', style: const pw.TextStyle(fontSize: 9)), ], ); }, ), ); // 生成PDF字节数据 final pdfBytes = await pdf.save(); // 上传到Firebase Storage final firebaseStorage = FirebaseStorage.instance; final reference = firebaseStorage.ref().child(fileName); // 使用putData直接上传字节数据,兼容Web端 final uploadTask = reference.putData(pdfBytes); final TaskSnapshot snapshot = await uploadTask; if (snapshot.state == TaskState.success) { final downloadUrl = await snapshot.ref.getDownloadURL(); print('PDF quotation uploaded to: $downloadUrl'); return downloadUrl; } else { throw FirebaseException( plugin: 'firebase_storage', code: 'upload-failed', message: 'PDF上传失败,请检查Firebase配置和网络连接。', ); } }
关键修改点
- 移除IO相关依赖:删除
dart:io导入、getApplicationDocumentsDirectory调用和本地文件保存逻辑,避免Web端不支持的API。 - 直接上传字节数据:使用
pdf.save()获取PDF字节数组,通过putData方法上传到Firebase Storage,替代原有的putFile。 - 移除重复Firebase初始化:删除
generateQuotationPDF中的await Firebase.initializeApp(),确保全局只初始化一次。 - 优化空值判断:在表格数据生成时增加
selectedItemNamesWithSKU[type]!.isNotEmpty判断,避免空字符串导致的异常。
额外注意事项
- 确保
shared_preferences在Web端的兼容性,该插件已支持Web,但需要在pubspec.yaml中正确配置。 - 检查Firebase Storage的规则,确保允许未认证用户上传(如果应用不需要登录),或者配置正确的认证逻辑。
内容的提问来源于stack exchange,提问作者Murtadha Alaali
相关产品推荐
相关产品推荐

