Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Create Custom Analytics in FlutterFlow

Custom analytics in FlutterFlow means building your own dashboard — not relying on third-party analytics tools. Use Firestore to store pre-aggregated metrics (daily totals, not raw event documents), fl_chart Custom Widgets for LineChart, BarChart, and PieChart visualizations, ChoiceChips for date range filters, and a Cloud Function to compute aggregates on a schedule so the dashboard loads instantly.

What you'll learn

  • How to design a Firestore schema for pre-aggregated analytics metrics
  • How to build KPI cards and bind them to real-time Firestore metrics data
  • How to create fl_chart line, bar, and pie chart Custom Widgets for data visualization
  • How to add date range filter ChoiceChips that update all dashboard widgets simultaneously
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner12 min read35-45 minFlutterFlow Pro+ (fl_chart Custom Widget and Cloud Functions required)March 2026RapidDev Engineering Team
TL;DR

Custom analytics in FlutterFlow means building your own dashboard — not relying on third-party analytics tools. Use Firestore to store pre-aggregated metrics (daily totals, not raw event documents), fl_chart Custom Widgets for LineChart, BarChart, and PieChart visualizations, ChoiceChips for date range filters, and a Cloud Function to compute aggregates on a schedule so the dashboard loads instantly.

Building an Analytics Dashboard That Scales

Many FlutterFlow developers make the mistake of loading raw event documents (page views, purchases, sign-ups) and aggregating them in the app on every dashboard load. With 50,000+ events, this becomes impossibly slow and expensive. The professional approach is to pre-aggregate: a Cloud Function runs nightly and writes daily totals to a small 'metrics' collection. The dashboard reads only the tiny metrics documents — never the raw events. This makes dashboards load in under one second regardless of event volume.

Prerequisites

  • A FlutterFlow project with Firestore connected and existing event or transaction data
  • Firebase project on the Blaze plan (Cloud Functions required for pre-aggregation)
  • fl_chart package familiarity is helpful but not required
  • Basic understanding of FlutterFlow Custom Widgets and Backend Queries

Step-by-step guide

1

Design the Pre-Aggregated Metrics Firestore Schema

Instead of querying thousands of raw event documents, store daily totals in a 'metrics' collection. Each document has a date-based ID (e.g., '2026-03-29') and fields for each metric: 'totalOrders' (Number), 'totalRevenue' (Number), 'newUsers' (Number), 'pageViews' (Number), 'conversionRate' (Number). Create a second collection 'metrics_summary' with a single 'current' document holding rolling totals: 'last7DaysRevenue', 'last30DaysRevenue', 'totalUsers', 'activeUsersToday'. In FlutterFlow's Firestore schema editor, add both collections and their fields. Your dashboard pages will only ever read from these two tiny collections — never from the raw events.

Expected result: The Firestore schema shows 'metrics' (daily docs) and 'metrics_summary' (rolling totals) collections. The Firebase Console shows sample data in these collections.

2

Build KPI Cards Bound to Firestore Metrics

In FlutterFlow, create a KPI card Component. It should have a title Text widget, a value Text widget, a change percentage Text widget (e.g., '+12% vs last week'), and a colored icon. Define Component Parameters: 'title' (String), 'value' (String), 'changePercent' (double), and 'changePositive' (boolean). Bind the change text's color to a conditional: if changePositive is true, use green; if false, use red. On your analytics dashboard page, add a Backend Query → Firestore Collection → 'metrics_summary', query type: Single Document, document ID: 'current'. Drag four KPI card Component instances onto a 2x2 grid and bind each card's parameters to fields from the query result.

Expected result: Four KPI cards on the dashboard show live values from Firestore's metrics_summary document. Editing the document in Firebase Console updates the cards within 1-2 seconds.

3

Create the LineChart Custom Widget with fl_chart

Add the 'fl_chart' package in Settings → Pubspec Dependencies. Create a Custom Widget named 'RevenueLineChart'. Define parameters: 'dataPoints' (List of double, the daily revenue values), 'labels' (List of String, the day labels), 'lineColor' (Color). Inside the widget, build a LineChart from fl_chart using LineChartData with a single LineChartBarData. Map the dataPoints list to FlSpot objects (index as x, value as y). Configure titlesData with bottom titles from the labels list and left titles showing formatted currency values. Style with gridData, borderData, and lineTouchData for interactive tooltips. On the dashboard page, add a Backend Query that fetches the last 7 or 30 days of daily metrics documents (ordered by document ID descending), extract the totalRevenue values into a list, and pass to the chart widget.

revenue_line_chart.dart
1// Custom Widget: RevenueLineChart
2// Parameters: dataPoints (List<double>), labels (List<String>), lineColor (Color)
3// Package: fl_chart
4
5class RevenueLineChart extends StatelessWidget {
6 final List<double> dataPoints;
7 final List<String> labels;
8 final Color lineColor;
9 final double height;
10
11 const RevenueLineChart({
12 Key? key,
13 required this.dataPoints,
14 required this.labels,
15 required this.lineColor,
16 this.height = 200.0,
17 }) : super(key: key);
18
19 @override
20 Widget build(BuildContext context) {
21 if (dataPoints.isEmpty) {
22 return SizedBox(
23 height: height,
24 child: const Center(child: Text('No data available')),
25 );
26 }
27
28 final spots = dataPoints
29 .asMap()
30 .entries
31 .map((e) => FlSpot(e.key.toDouble(), e.value))
32 .toList();
33
34 final maxY = dataPoints.reduce((a, b) => a > b ? a : b) * 1.2;
35
36 return SizedBox(
37 height: height,
38 child: LineChart(
39 LineChartData(
40 gridData: FlGridData(
41 show: true,
42 drawVerticalLine: false,
43 getDrawingHorizontalLine: (value) => FlLine(
44 color: Colors.grey.withOpacity(0.2),
45 strokeWidth: 1,
46 ),
47 ),
48 titlesData: FlTitlesData(
49 leftTitles: AxisTitles(
50 sideTitles: SideTitles(
51 showTitles: true,
52 reservedSize: 50,
53 getTitlesWidget: (value, meta) => Text(
54 '\$${value.toStringAsFixed(0)}',
55 style: const TextStyle(fontSize: 10),
56 ),
57 ),
58 ),
59 bottomTitles: AxisTitles(
60 sideTitles: SideTitles(
61 showTitles: true,
62 reservedSize: 22,
63 getTitlesWidget: (value, meta) {
64 final idx = value.toInt();
65 if (idx < 0 || idx >= labels.length) return const SizedBox();
66 return Text(labels[idx],
67 style: const TextStyle(fontSize: 10));
68 },
69 ),
70 ),
71 topTitles: const AxisTitles(
72 sideTitles: SideTitles(showTitles: false)),
73 rightTitles: const AxisTitles(
74 sideTitles: SideTitles(showTitles: false)),
75 ),
76 borderData: FlBorderData(show: false),
77 minX: 0,
78 maxX: (dataPoints.length - 1).toDouble(),
79 minY: 0,
80 maxY: maxY,
81 lineBarsData: [
82 LineChartBarData(
83 spots: spots,
84 isCurved: true,
85 color: lineColor,
86 barWidth: 2.5,
87 dotData: const FlDotData(show: false),
88 belowBarData: BarAreaData(
89 show: true,
90 color: lineColor.withOpacity(0.1),
91 ),
92 ),
93 ],
94 lineTouchData: LineTouchData(
95 touchTooltipData: LineTouchTooltipData(
96 getTooltipItems: (spots) => spots
97 .map((s) => LineTooltipItem(
98 '\$${s.y.toStringAsFixed(2)}',
99 const TextStyle(
100 color: Colors.white,
101 fontWeight: FontWeight.bold),
102 ))
103 .toList(),
104 ),
105 ),
106 ),
107 ),
108 );
109 }
110}

Expected result: The dashboard shows a smooth line chart with daily revenue values. Tapping a data point shows a tooltip with the exact value.

4

Add Date Range Filter with ChoiceChips

A static dashboard with fixed data is not useful. Add a date range filter row at the top of the dashboard. In FlutterFlow, drag a Row widget. Inside, add three ChoiceChip widgets (or use a row of styled Button widgets): '7 Days', '30 Days', '90 Days'. Create a Page State variable 'selectedRange' (String, initial value '7'). Wire each chip's On Select action to Update Page State → set selectedRange to '7', '30', or '90'. Add a Condition to each chip's styling: if selectedRange equals this chip's value, show the active/selected style (filled background, white text); otherwise show the inactive style (outline only). Now wire your Backend Query on the metrics collection to a filter: document ID >= dateSubtract(today, selectedRange days). The chart and KPI cards update automatically when selectedRange changes.

Expected result: Tapping '30 Days' updates the Backend Query filter, and the chart animates to show 30 data points. The active chip has a filled style. KPI values update to reflect the selected range.

5

Deploy the Pre-Aggregation Cloud Function

This Cloud Function runs nightly (or on demand) to compute aggregate totals from your raw event documents and write them to the 'metrics' daily documents and 'metrics_summary' current document. It groups raw events by date, sums revenue and counts orders, and updates only the records that need updating. This runs once per day server-side — so no matter how many events you have, the dashboard always reads from the tiny pre-computed documents. Deploy via Firebase CLI. To trigger it manually during development: in Firebase Console → Functions → trigger manually. To update the summary stats (last 7/30 days), the function reads from the already-computed daily docs instead of raw events — extremely fast.

aggregateMetrics.js
1// Firebase Cloud Function: aggregateMetrics
2// Scheduled: daily at midnight
3// exports.aggregateMetrics = functions.pubsub
4// .schedule('0 0 * * *').onRun(async () => { ... })
5
6const functions = require('firebase-functions');
7const admin = require('firebase-admin');
8
9exports.aggregateMetrics = functions.pubsub
10 .schedule('0 0 * * *')
11 .onRun(async () => {
12 const db = admin.firestore();
13 const today = new Date();
14 today.setHours(0, 0, 0, 0);
15 const dateStr = today.toISOString().split('T')[0];
16
17 // Aggregate raw events for today
18 const eventsSnap = await db.collection('events')
19 .where('createdAt', '>=', today)
20 .get();
21
22 let totalOrders = 0, totalRevenue = 0, newUsers = 0;
23 eventsSnap.forEach(doc => {
24 const d = doc.data();
25 if (d.type === 'order_completed') {
26 totalOrders++;
27 totalRevenue += d.amount || 0;
28 }
29 if (d.type === 'user_signup') newUsers++;
30 });
31
32 // Write daily metrics document
33 await db.collection('metrics').doc(dateStr).set({
34 totalOrders, totalRevenue, newUsers,
35 date: dateStr,
36 updatedAt: admin.firestore.FieldValue.serverTimestamp(),
37 }, { merge: true });
38
39 // Update rolling summary
40 const last30 = await db.collection('metrics')
41 .orderBy('__name__', 'desc').limit(30).get();
42 let last30Revenue = 0;
43 last30.forEach(d => { last30Revenue += d.data().totalRevenue || 0; });
44
45 await db.collection('metrics_summary').doc('current').set({
46 last30DaysRevenue: last30Revenue,
47 updatedAt: admin.firestore.FieldValue.serverTimestamp(),
48 }, { merge: true });
49
50 return null;
51 });

Expected result: The Cloud Function runs daily and updates 'metrics' daily documents and 'metrics_summary' current document. Dashboard load time drops to under 1 second.

Complete working example

analytics_dashboard.dart
1// ============================================================
2// FlutterFlow Custom Analytics — KPI Card Component
3// ============================================================
4// This is a reusable KPI card widget used on the dashboard
5// Parameters: title, value, changePercent, isPositiveChange
6
7class KpiCard extends StatelessWidget {
8 final String title;
9 final String value;
10 final double changePercent;
11 final bool isPositiveChange;
12 final IconData icon;
13 final Color accentColor;
14
15 const KpiCard({
16 Key? key,
17 required this.title,
18 required this.value,
19 required this.changePercent,
20 required this.isPositiveChange,
21 required this.icon,
22 required this.accentColor,
23 }) : super(key: key);
24
25 @override
26 Widget build(BuildContext context) {
27 final changeColor = isPositiveChange ? Colors.green : Colors.red;
28 final changePrefix = isPositiveChange ? '+' : '';
29
30 return Container(
31 padding: const EdgeInsets.all(16),
32 decoration: BoxDecoration(
33 color: Colors.white,
34 borderRadius: BorderRadius.circular(12),
35 boxShadow: [
36 BoxShadow(
37 color: Colors.black.withOpacity(0.06),
38 blurRadius: 10,
39 offset: const Offset(0, 2),
40 ),
41 ],
42 ),
43 child: Column(
44 crossAxisAlignment: CrossAxisAlignment.start,
45 children: [
46 Row(
47 mainAxisAlignment: MainAxisAlignment.spaceBetween,
48 children: [
49 Text(title,
50 style: const TextStyle(
51 fontSize: 13,
52 color: Colors.grey,
53 fontWeight: FontWeight.w500)),
54 Icon(icon, color: accentColor, size: 20),
55 ],
56 ),
57 const SizedBox(height: 8),
58 Text(value,
59 style: const TextStyle(
60 fontSize: 24, fontWeight: FontWeight.bold)),
61 const SizedBox(height: 4),
62 Row(
63 children: [
64 Icon(
65 isPositiveChange
66 ? Icons.trending_up
67 : Icons.trending_down,
68 color: changeColor,
69 size: 14,
70 ),
71 const SizedBox(width: 4),
72 Text(
73 '$changePrefix${changePercent.toStringAsFixed(1)}% vs last period',
74 style: TextStyle(fontSize: 11, color: changeColor),
75 ),
76 ],
77 ),
78 ],
79 ),
80 );
81 }
82}

Common mistakes when creating Custom Analytics in FlutterFlow

Why it's a problem: Running real-time aggregate queries on raw event documents (counting 50K+ docs on each dashboard load)

How to avoid: Pre-aggregate using a nightly Cloud Function that writes daily totals to a 'metrics' collection. The dashboard only reads the tiny pre-computed metric documents — typically 7-30 documents for a date range query. Dashboard load time drops from 5-10 seconds to under 500 milliseconds.

Why it's a problem: Creating a new Backend Query for each KPI card instead of one shared query for the page

How to avoid: Set one Backend Query on the page root widget that fetches the metrics_summary document. Pass its fields down to all KPI card Components via Component Parameters. One query, eight cards.

Why it's a problem: Passing empty lists to fl_chart without null checks

How to avoid: Add a null/empty check in the Custom Widget: if (dataPoints.isEmpty) { return a placeholder widget } before constructing the chart. Also add a loading state check on the page: show a loading shimmer while the Backend Query is still fetching.

Why it's a problem: Using the same date range filter ChoiceChip logic to also reload the Firestore backend query

How to avoid: Use a Custom Action to re-query Firestore when the filter changes, passing the dynamic start date as a parameter. Or use Conditional Widgets: show different Backend Query widgets for each date range, and show/hide them based on selectedRange Page State.

Best practices

  • Pre-aggregate all metrics server-side — never compute totals by reading raw event documents in the client app.
  • Use date-keyed document IDs (YYYY-MM-DD) in the metrics collection so date range queries become simple string comparisons.
  • Show loading skeletons (shimmer placeholders) while Backend Queries are fetching — a blank dashboard looks broken.
  • Limit date range options to reasonable periods (7/30/90 days) — all-time queries on large datasets will always be slow.
  • Cache the last fetched metrics in App State so the dashboard shows data immediately on revisit while a fresh query runs in the background.
  • Add refresh pull-to-refresh on the dashboard page so users can manually trigger a data refresh.
  • Use fl_chart's animation duration parameter to animate charts when data changes — smooth transitions make dashboards feel more polished and data-driven.
  • For multi-tenant apps (each business sees their own data), include a tenantId field in all metrics documents and filter all queries by it. Consider using RapidDev for help architecting multi-tenant analytics schemas on Firestore.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a custom analytics dashboard in FlutterFlow backed by Firestore. I have raw event documents and want to pre-aggregate them into daily metric totals using a Firebase Cloud Function. Write a Node.js Cloud Function that reads events from today from a 'events' collection, computes totalOrders, totalRevenue, and newUsers, and writes the results to a 'metrics' document keyed by today's date in YYYY-MM-DD format.

FlutterFlow Prompt

Write a FlutterFlow Custom Widget in Dart called RevenueLineChart using fl_chart that accepts a List<double> dataPoints and List<String> labels parameter. It should render a LineChart with a smooth curve, area fill below the line, left axis labels formatted as dollar amounts, and touch tooltips showing exact values.

Frequently asked questions

Do I need Firebase Blaze plan for a custom analytics dashboard?

Only if you want pre-aggregation Cloud Functions, which require Blaze. The dashboard UI and Firestore reads work on the Spark (free) plan. However, without Cloud Functions, you would need to aggregate in the client app (slow, expensive), so Blaze is strongly recommended for any analytics dashboard with meaningful data volume.

Can I use Google Analytics (GA4) instead of building custom analytics?

For standard user behavior tracking (screen views, events, conversions), GA4 is much easier and free. Custom analytics in Firestore is the right choice when you need business-specific metrics (revenue per product, orders per region, subscription churn rate) that GA4 does not provide out of the box, or when you want to display analytics to your own users inside the app.

How do I update the dashboard without refreshing the page?

Use a Firestore real-time stream (Backend Query with Single Time Query disabled) on the metrics_summary document. FlutterFlow's real-time queries automatically re-render widgets when Firestore data changes. When the Cloud Function updates the metrics, the dashboard updates automatically within 1-2 seconds without any user action.

How do I make the fl_chart widget responsive to different screen sizes?

Wrap the Custom Widget in a Container with no fixed width and use LayoutBuilder inside the widget to get the available width. Pass the width as a parameter to the chart so it can calculate correct axis spacing. Alternatively, use MediaQuery.of(context).size.width inside the widget to get screen dimensions.

Can I export the dashboard data to CSV from FlutterFlow?

Yes, via a Custom Action. The action reads the metrics documents, formats them as CSV rows, and uses the 'share_plus' package to share or 'path_provider' + 'dart:io' to save the CSV file locally. You can then trigger the share sheet to let users export to their preferred app.

What is the maximum number of data points fl_chart can render smoothly?

fl_chart renders up to around 500 data points smoothly on modern devices. Beyond 500, animation and touch performance degrade noticeably. For 365-day annual charts, consider rendering weekly aggregates (52 points) rather than daily points (365 points) for better performance.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.