Build a self-service report builder in FlutterFlow with a multi-step configuration UI — users select data source, date range, grouping, and chart type. A Firebase Cloud Function runs the aggregation query server-side and returns summarized data. The app renders charts with fl_chart and exports to PDF or CSV using pdf and csv packages. Never aggregate large date ranges on the client.
Why Server-Side Aggregation Is Essential for Reports
Reports summarize large amounts of data — often thousands or millions of records. If you query all raw documents to the device and aggregate them in Dart, you'll exhaust memory on mobile devices, burn through Firestore read quotas, and produce 10-30 second loading times. The correct architecture is: device sends report configuration parameters to a Cloud Function, the function runs the aggregation in Firestore (or BigQuery for very large datasets), and returns a compact summary JSON. The device only receives the final summarized data for display and export.
Prerequisites
- A FlutterFlow project with Firebase Firestore connected and meaningful transactional or event data to report on
- Firebase project on the Blaze plan (Cloud Functions required for server-side aggregation)
- FlutterFlow Pro or higher for Custom Actions and code export
- Basic understanding of Firestore queries and data structure
Step-by-step guide
Design the report configuration UI with multi-step selector
Design the report configuration UI with multi-step selector
Create a new page named 'Report Builder'. Use a PageView widget with 4 pages representing the configuration steps. Step 1: data source selector — a ListView of available report types (Sales Summary, User Activity, Order Status, Custom). Step 2: date range picker — two DatePicker widgets for start and end date, plus Quick Select chips (Last 7 Days, Last 30 Days, Last Quarter, Custom). Step 3: columns and grouping — a CheckboxListTile per available column (date, category, amount, count, region) and a DropdownButton for groupBy (Day, Week, Month, Category). Step 4: chart type selector — icon cards for Bar Chart, Line Chart, Pie Chart, Table Only. Store all selections in Page State variables. A sticky bottom bar shows the current step number and 'Next' / 'Generate Report' buttons.
Expected result: Users can step through the 4-page configuration, see their selections summarized on the final step, and tap 'Generate Report'.
Create a Cloud Function for server-side aggregation
Create a Cloud Function for server-side aggregation
In your Firebase Functions directory, create generateReport.js. The function accepts: dataSource (String), startDate (Timestamp), endDate (Timestamp), groupBy (String), columns (String Array), and userId (String for security). Based on dataSource, query the appropriate Firestore collection filtering by a timestamp field between startDate and endDate. Group results in JavaScript — build a Map keyed by the groupBy period (e.g., week number, month-year, or category name). For each group, compute: total count, sum of numeric fields in the requested columns, and average where applicable. Return the aggregated data as a flat JSON array — one object per group — with the group key and all computed values. Keep the response under 1MB for safe client delivery.
1// Cloud Function: generateReport2const functions = require('firebase-functions');3const admin = require('firebase-admin');45exports.generateReport = functions.https.onCall(6 async (data, context) => {7 if (!context.auth) {8 throw new functions.https.HttpsError(9 'unauthenticated', 'Login required'10 );11 }12 const { dataSource, startDate, endDate, groupBy, columns } = data;13 const db = admin.firestore();14 const snap = await db15 .collection(dataSource)16 .where('createdAt', '>=', new Date(startDate))17 .where('createdAt', '<=', new Date(endDate))18 .where('userId', '==', context.auth.uid)19 .get();2021 const groups = {};22 snap.forEach((doc) => {23 const d = doc.data();24 const date = d.createdAt.toDate();25 let key;26 if (groupBy === 'day') {27 key = date.toISOString().split('T')[0];28 } else if (groupBy === 'week') {29 const week = Math.ceil(date.getDate() / 7);30 key = `${date.getFullYear()}-W${String(week).padStart(2, '0')}`;31 } else if (groupBy === 'month') {32 key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;33 } else {34 key = d[groupBy] || 'Other';35 }36 if (!groups[key]) {37 groups[key] = { period: key, count: 0, totalAmount: 0 };38 }39 groups[key].count++;40 if (columns.includes('amount') && d.amount) {41 groups[key].totalAmount += d.amount;42 }43 });44 return Object.values(groups)45 .sort((a, b) => a.period.localeCompare(b.period));46 }47);Expected result: Calling the Cloud Function from the app returns a sorted array of grouped report data within 2-5 seconds for typical data volumes.
Display chart results using fl_chart Custom Widget
Display chart results using fl_chart Custom Widget
After the Cloud Function returns data, store it in a Page State variable (JSON List). Create a Custom Widget named 'ReportChart' that accepts: chartType (String), data (List of Maps), xKey (String), yKey (String). Use the fl_chart package to render the appropriate chart type — BarChart for bar, LineChart for line, PieChart for pie. Map the incoming data array to fl_chart's data model: for BarChart, create BarChartGroupData for each data point using the group index as x and the numeric value as y. Show a loading spinner while the Cloud Function is running. Below the chart, add a DataTable widget showing the same data in tabular format, using FlutterFlow's built-in DataTable component or a ListView of Row widgets.
Expected result: After report generation, the selected chart type renders with labeled axes and the correct data points from the Cloud Function response.
Implement PDF and CSV export
Implement PDF and CSV export
Add an Export button with a DropdownButton offering PDF and CSV options. For CSV: create a Custom Action named 'exportCsv' that takes the report data List and column names, builds a CSV string using the csv package (or manual join), saves it to a temporary file using path_provider, and shares it via share_plus. For PDF: create a Custom Action named 'exportPdf' that uses the pdf package to build a Document with a table layout. Add a header row with column names and a data row for each report group. Convert to bytes and share. Include the report title, date range, and generation timestamp in the PDF header.
1// Custom Action: exportCsv2import 'dart:io';3import 'package:csv/csv.dart';4import 'package:path_provider/path_provider.dart';5import 'package:share_plus/share_plus.dart';67Future<void> exportCsv(8 List<dynamic> reportData,9 List<String> columns,10 String reportTitle,11) async {12 final rows = <List<dynamic>>[13 columns, // header row14 ...reportData.map((row) =>15 columns.map((col) => row[col] ?? '').toList()),16 ];17 final csvString = const ListToCsvConverter().convert(rows);18 final dir = await getTemporaryDirectory();19 final fileName =20 '${reportTitle.replaceAll(' ', '_')}_${DateTime.now().millisecondsSinceEpoch}.csv';21 final file = File('${dir.path}/$fileName');22 await file.writeAsString(csvString);23 await Share.shareXFiles(24 [XFile(file.path)],25 subject: reportTitle,26 );27}Expected result: Tapping 'Export CSV' generates a properly formatted CSV file and opens the share sheet. The CSV opens correctly in Excel or Google Sheets.
Save and restore report templates in Firestore
Save and restore report templates in Firestore
Add a 'Save as Template' button on the report configuration screen. On tap, show a TextField dialog asking for a template name. On confirm, write a document to Firestore collection 'reportTemplates' containing: name, dataSource, startDateOffset (integer days from today), endDateOffset, groupBy, columns, chartType, and userId. On the Report Builder home screen, show a 'Saved Templates' section with a list of the user's templates fetched from Firestore. Tapping a template populates all Page State variables with the saved configuration and navigates to step 4, ready to generate. Include a delete button on each template card.
Expected result: Saved templates appear in the list and can be loaded to instantly pre-fill the report configuration form.
Complete working example
1// Report Builder Custom Actions2// Add to FlutterFlow Custom Code panel34import 'dart:io';5import 'package:csv/csv.dart';6import 'package:path_provider/path_provider.dart';7import 'package:share_plus/share_plus.dart';8import 'package:pdf/pdf.dart';9import 'package:pdf/widgets.dart' as pw;1011// ─── Export to CSV ────────────────────────────────────────────────────────────12Future<void> exportCsv(13 List<dynamic> reportData,14 List<String> columns,15 String reportTitle,16) async {17 final rows = <List<dynamic>>[18 columns,19 ...reportData.map(20 (row) => columns.map((col) => (row as Map)[col] ?? '').toList(),21 ),22 ];23 final csvString = const ListToCsvConverter().convert(rows);24 final dir = await getTemporaryDirectory();25 final fileName =26 '${reportTitle.replaceAll(' ', '_')}_${DateTime.now().millisecondsSinceEpoch}.csv';27 final file = File('${dir.path}/$fileName');28 await file.writeAsString(csvString);29 await Share.shareXFiles([XFile(file.path)], subject: reportTitle);30}3132// ─── Export to PDF ────────────────────────────────────────────────────────────33Future<void> exportPdf(34 List<dynamic> reportData,35 List<String> columns,36 String reportTitle,37 String dateRange,38) async {39 final doc = pw.Document();40 doc.addPage(41 pw.MultiPage(42 build: (context) => [43 pw.Header(44 level: 0,45 child: pw.Text(46 reportTitle,47 style: pw.TextStyle(fontSize: 20, fontWeight: pw.FontWeight.bold),48 ),49 ),50 pw.Text('Period: $dateRange',51 style: const pw.TextStyle(fontSize: 12)),52 pw.SizedBox(height: 12),53 pw.Table.fromTextArray(54 headers: columns,55 data: reportData56 .map((row) =>57 columns.map((col) => '${(row as Map)[col] ?? ''}')58 .toList())59 .toList(),60 ),61 pw.SizedBox(height: 8),62 pw.Text(63 'Generated: ${DateTime.now().toString()}',64 style: pw.TextStyle(65 fontSize: 10,66 color: PdfColors.grey,67 ),68 ),69 ],70 ),71 );72 final bytes = await doc.save();73 final dir = await getTemporaryDirectory();74 final file = File(75 '${dir.path}/${reportTitle.replaceAll(' ', '_')}_${DateTime.now().millisecondsSinceEpoch}.pdf',76 );77 await file.writeAsBytes(bytes);78 await Share.shareXFiles([XFile(file.path)], subject: reportTitle);79}8081// ─── Validate date range ──────────────────────────────────────────────────────82bool isDateRangeValid(DateTime start, DateTime end) {83 if (end.isBefore(start)) return false;84 final diff = end.difference(start).inDays;85 return diff <= 365;86}Common mistakes when creating a Custom Report Generation Tool in FlutterFlow
Why it's a problem: Querying all raw Firestore documents for a large date range and aggregating on the client device
How to avoid: Always run aggregation in a Firebase Cloud Function. The function runs in Google's infrastructure close to Firestore, processes data in-memory server-side, and returns only the compact summary (typically 12-365 rows) to the device.
Why it's a problem: Using Dart's DateTime comparison for grouping without handling timezone offsets
How to avoid: Accept the user's UTC offset as a parameter in your Cloud Function. Apply the offset when computing the group key so that midnight-to-midnight grouping matches the user's local calendar.
Why it's a problem: Generating PDF documents on the main UI thread, blocking the interface
How to avoid: Run PDF generation in a separate Dart isolate using compute() or in a background isolate. Show a loading indicator while generation is in progress.
Best practices
- Always enforce a maximum date range in both the UI (client-side validation) and the Cloud Function (server-side guard) to prevent accidental runaway queries.
- Cache Cloud Function report results in Firestore for 15-30 minutes — if the user regenerates the same report twice, return the cached result instead of re-querying.
- Limit exported CSV/PDF to 10,000 rows maximum and paginate larger datasets across multiple exports to prevent file size issues.
- Include a report metadata section in every PDF export: title, date range, applied filters, total record count, and generation timestamp for audit purposes.
- Use Firestore Security Rules to ensure users can only generate reports on their own data, never other users' data, even if they manipulate the API call parameters.
- Design report templates to store relative date offsets ('last 30 days') rather than absolute dates so saved templates remain useful when run at different times.
- Test report performance with realistic data volumes — load your Firestore test collection with 10,000+ documents before performance testing the Cloud Function.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a Firebase Cloud Function that accepts a report configuration (dataSource, startDate, endDate, groupBy as 'day'/'month'/'category', columns as array) and returns aggregated Firestore data. The collection has documents with fields: createdAt (Timestamp), amount (Number), category (String), userId (String). Write the complete Cloud Function that queries this collection, groups results by the specified period, computes count and sum of amount per group, and returns a sorted array of group objects.
I have a FlutterFlow report builder page. After a Cloud Function returns an array of report data objects like [{period: '2026-01', count: 42, totalAmount: 1250.00}, ...], I need to display this in a bar chart using the fl_chart package in a Custom Widget. Write me the Custom Widget code that renders a BarChart with period labels on the x-axis and totalAmount on the y-axis.
Frequently asked questions
Can I build this report generator without Firebase Cloud Functions, using only Firestore client queries?
For small datasets (under 1,000 documents), yes — Firestore client queries can aggregate data on-device without significant performance issues. For anything larger, Cloud Functions are required. Firestore also added native aggregation queries (count(), sum(), average()) in 2023 that work client-side without pulling all documents.
How do I let users email the report instead of sharing it?
Use the mailer or flutter_email_sender package to compose an email with the PDF or CSV file attached. Alternatively, use a Cloud Function to send the email server-side via SendGrid or Mailgun, which allows HTML report formatting and reliable delivery without requiring a native email client on the device.
Can I schedule reports to run automatically and be emailed on a schedule?
Yes. Create a Firebase Cloud Function with a pub.sub schedule trigger (e.g., every Monday at 8am). The function reads saved report template configurations from Firestore, runs each report for all subscribed users, generates a PDF server-side using a Node.js PDF library, and emails it via SendGrid. This is an advanced use case but follows the same aggregation pattern.
How do I handle the case where the Cloud Function takes more than 60 seconds for large reports?
Cloud Functions have a maximum timeout of 540 seconds (9 minutes) for 1st gen and 3600 seconds (60 minutes) for 2nd gen. Increase the timeout in your function configuration. For very large reports, consider breaking the work into smaller chunks using Firestore pagination and writing intermediate results to a temporary Firestore document, then assembling the final result.
What chart library works best for reports in a FlutterFlow exported project?
fl_chart is the most widely used Flutter chart library with good documentation and support for bar, line, pie, and scatter charts. syncfusion_flutter_charts is more feature-rich (including Excel-like chart options) but requires a commercial license for production use. For simple bar/line charts, fl_chart covers most report use cases.
How do I show a real-time progress indicator while the report is generating?
Cloud Function calls are asynchronous one-shot HTTP requests that return a single response — they do not support streaming progress updates. Work around this by writing progress checkpoints to a Firestore document (e.g., {progress: 0.3, status: 'Aggregating data'}) from within the Cloud Function, and use a Firestore real-time listener in your app to display the progress percentage while the function runs.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation