I am developing a quotation maker app using Flutter for the company I work with. The app generates a PDF document based on the user’s selections and stores it in Firebase Storage. The PDF generation and storage work perfectly fine on macOS, but I encounter an issue when running the app on Flutter Web.
Problem:
The issue arises specifically on Flutter Web. When attempting to store the generated PDF file in Firebase Storage, the app throws an error: completer.complete. The error message doesn’t provide much information about the root cause of the problem, making it challenging for me to pinpoint the exact issue.
Code:
I have shared the code that performs the PDF generation and storage in Firebase Storage on
pdf_generator.dart
import 'package:flutter/services.dart';
import 'package:pdf/pdf.dart';
import 'package:path_provider/path_provider.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';
import 'dart:io';
// Function to get the current quotation number
Future<int> _getQuotationNumber() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
int quotationNumber = prefs.getInt('quotation_number') ?? 0;
return quotationNumber;
}
// Function to increment the quotation number
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 {
// Initialize Firebase
await Firebase.initializeApp();
final pdf = pw.Document();
// Create the PDF file name with the quotation number
String directoryName = 'quotation_pdfs';
final appDocumentsDir = await getApplicationDocumentsDirectory();
String directoryPath = '${appDocumentsDir.path}/$directoryName';
Directory(directoryPath).create(recursive: true);
// Increment the quotation number
await _incrementQuotationNumber();
int quotationNumber = await _getQuotationNumber();
String fileName =
'$directoryName/PC-${quotationNumber.toString().padLeft(3, '0')}.pdf';
final pdfPath = '${appDocumentsDir.path}/$fileName';
// Load the logo image
final logoImage = pw.MemoryImage(
(await rootBundle.load('assets/cazalogo.webp')).buffer.asUint8List(),
);
// Get the current date
final currentDate = DateTime.now();
// Format the date to show only the date part (YYYY-MM-DD)
final formattedDate =
"${currentDate.year}-${currentDate.month.toString().padLeft(2, '0')}-${currentDate.day.toString().padLeft(2, '0')}";
// Add content to the PDF document
pdf.addPage(
pw.Page(
build: (pw.Context context) {
final List<List<String?>> tableData = [
['Item', 'SKU', 'Qty', 'Cost', 'Selling Price'], // Header row
for (String type in selectedItemNamesWithSKU.keys)
if (selectedItemNamesWithSKU[type] != null &&
quantities[selectedItemNamesWithSKU[type]] != 0)
[
selectedItemNamesWithSKU[type], // Item name with SKU
itemSku[selectedItemNamesWithSKU[type]] ?? '', // SKU
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: [
// Display the logo/image
pw.Center(
child: pw.Image(logoImage, width: 50, height: 50),
),
// Display the quotation number
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)),
// Display the date of creation
pw.Text('$formattedDate',
style: const pw.TextStyle(
fontSize: 10)), // Adjust font size if needed
pw.SizedBox(height: 20),
pw.Text('Quotation Details',
style: const pw.TextStyle(fontSize: 15)),
pw.Divider(),
pw.SizedBox(height: 10),
// Display the chosen items only in the table
// ignore: deprecated_member_use
pw.Table.fromTextArray(
headerStyle:
pw.TextStyle(fontWeight: pw.FontWeight.bold, fontSize: 10),
cellStyle: const pw.TextStyle(
fontSize: 7), // Smaller font size for items in the table
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,
// Set the widths of each column here using the 'columnWidths' property
columnWidths: {
0: const pw.FlexColumnWidth(5), // Item column width
1: const pw.FlexColumnWidth(2), // SKU column width
2: const pw.FlexColumnWidth(1), // Quantity column width
3: const pw.FlexColumnWidth(2), // Cost (BHD) column width
4: const pw.FlexColumnWidth(
2), // Selling Price (BHD) column width
},
),
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)),
],
);
},
),
);
// Save the PDF file to a file
final file = File(pdfPath);
await file.writeAsBytes(await pdf.save());
// Store the PDF file in Firestore
final firebaseStorage = FirebaseStorage.instance;
final reference = firebaseStorage.ref().child(fileName);
final uploadTask = reference.putFile(file);
// Wait for the upload to complete
final TaskSnapshot snapshot = await uploadTask;
// Check if the upload was successful
if (snapshot.state == TaskState.success) {
// Get the download URL of the uploaded PDF file
final downloadUrl = await snapshot.ref.getDownloadURL();
// Optionally, show a message or perform other actions after the PDF is uploaded
print('PDF quotation uploaded to: $downloadUrl');
// Return the download link
return downloadUrl;
} else {
throw FirebaseException(
plugin: 'firebase_storage',
code: 'object-not-found',
message: 'No object exists at the desired reference.');
}
}
pcbuilder.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:google_fonts/google_fonts.dart';
import '/functions/pdf_generator.dart';
import '/functions/product_data_loader.dart';
import '/functions/product_calculations.dart';
import 'package:firebase_core/firebase_core.dart';
import '/model/pcparts.dart';
class PCBuilderExcel extends StatefulWidget {
@override
_PCBuilderExcelState createState() => _PCBuilderExcelState();
}
enum PdfGenerationState {
notStarted,
loading,
success,
error,
}
void main() async {
// Pass the Firebase web configuration options
var firebaseOptions = const FirebaseOptions(
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
measurementId: "",
);
// Make sure to call WidgetsFlutterBinding.ensureInitialized() before Firebase.initializeApp()
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: firebaseOptions);
runApp(PCBuilderExcel());
}
class _PCBuilderExcelState extends State<PCBuilderExcel> {
Map<String, double> itemPrices = {};
Map<String, String> itemSku = {};
Map<String, String> selectedItemNames = {};
Map<String, int> quantities = {};
Map<String, double> sellingPrices = {};
double discount = 0.0;
double totalCost = 0.0;
double totalSellingPrice = 0.0;
List<String> allItemNames = [];
Map<String, double> itemCostPrices = {};
// Define a Map to hold the TextEditingController for each item
Map<String, TextEditingController> quantityControllers = {};
bool showExtraCards = false; // Flag to show extra cards
void initState() {
super.initState();
loadProductData(
allItemNames,
itemCostPrices,
itemPrices,
itemSku,
selectedItemNames,
quantities,
_updateState,
);
// Initialize the quantityControllers with default values set to '1'
for (var item in allItemNames) {
quantityControllers[item] = TextEditingController(text: '1');
}
// Initialize the selectedItemNames map with the null value for all items
selectedItemNames = {};
for (var item in allItemNames) {
selectedItemNames[item] = ''; // Use an empty string instead of null
}
}
void _updateState() {
setState(() {});
}
double calculateTotalCost() {
return ProductCalculations.calculateTotalCost(
selectedItemNames,
itemCostPrices,
quantities,
);
}
double calculateTotalSellingPrice() {
return ProductCalculations.calculateTotalSellingPrice(
selectedItemNames,
sellingPrices,
);
}
double calculateMargin() {
return ProductCalculations.calculateMargin(
selectedItemNames,
sellingPrices,
itemCostPrices,
quantities,
);
}
double calculateSellingPriceAfterDiscount() {
return ProductCalculations.calculateSellingPriceAfterDiscount(
selectedItemNames,
sellingPrices,
discount,
);
}
double calculateMarginAfterDiscount() {
return ProductCalculations.calculateMarginAfterDiscount(
selectedItemNames,
sellingPrices,
itemCostPrices,
quantities,
discount,
);
}
void updateSellingPrice(String type) {
ProductCalculations.updateSellingPrice(
type,
selectedItemNames,
itemPrices,
sellingPrices,
itemCostPrices,
quantities,
() {
setState(() {});
},
);
}
void _onGenerateQuotationButtonPressed() async {
// Call the function to generate the PDF quotation
await generateQuotationPDF(
selectedItemNames, // Pass selectedItemNames as selectedItemNamesWithSKU
quantities,
sellingPrices,
itemSku,
itemCostPrices,
itemPrices,
calculateTotalCost(),
calculateTotalSellingPrice(),
discount,
);
}
// Define the getIconForType method here
IconData getIconForType(String type) {
switch (type) {
case 'CPU':
return Icons.computer;
case 'GPU':
return Icons.videogame_asset;
case 'RAM':
return Icons.memory;
case 'Storage':
return Icons.storage;
case 'Motherboard':
return Icons.device_hub;
default:
return Icons.category;
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'PC Builder Excel',
theme: ThemeData(
primarySwatch: Colors.blue,
// Use the NotoSans font as the default font
fontFamily: GoogleFonts.notoSans().fontFamily,
),
home: Scaffold(
appBar: AppBar(
title: const Text('PC Builder Excel'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 10),
ElevatedButton(
onPressed: _onGenerateQuotationButtonPressed,
child: const Text('Generate Quotation'),
),
2
Answers
the code works with me when i added storageBucket line to the main
you should call Firebase.initializeApp once in app.
so you have to call Firebase.initializeApp in main function only.