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

How to Create a Custom Analytics Widget for Your FlutterFlow App

Build an analytics dashboard Component in FlutterFlow with KPI cards showing total users, revenue, and active sessions. Each card displays an icon, metric value, label, and a color-coded percentage change indicator — green arrow up or red arrow down. A trend line chart built with fl_chart visualizes data over time. All data comes from a pre-aggregated analytics_daily Firestore collection populated by a scheduled Cloud Function, so queries stay fast and cheap. A ChoiceChips date range filter lets users toggle between Today, Week, and Month views.

What you'll learn

  • How to design a Firestore analytics_daily collection with a Cloud Function aggregation pattern
  • How to build a reusable KPI metric card Component with percentage change calculation
  • How to create a trend line chart Custom Widget using fl_chart's LineChart
  • How to wire a date range filter with ChoiceChips to re-query both KPI cards and chart data
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate11 min read25-35 minFlutterFlow Pro+ (Custom Widget and Custom Function required)March 2026RapidDev Engineering Team
TL;DR

Build an analytics dashboard Component in FlutterFlow with KPI cards showing total users, revenue, and active sessions. Each card displays an icon, metric value, label, and a color-coded percentage change indicator — green arrow up or red arrow down. A trend line chart built with fl_chart visualizes data over time. All data comes from a pre-aggregated analytics_daily Firestore collection populated by a scheduled Cloud Function, so queries stay fast and cheap. A ChoiceChips date range filter lets users toggle between Today, Week, and Month views.

KPI dashboard with metric cards, trend chart, and date filtering

Most analytics dashboards in FlutterFlow fail because they try to aggregate raw event data in real-time — expensive Firestore reads, slow rendering, and unpredictable costs. This tutorial takes the production approach: a scheduled Cloud Function pre-computes daily totals into an analytics_daily collection. The FlutterFlow UI reads these lightweight summary documents. You will build reusable KPI metric card Components that accept parameters for label, current value, and previous value, then calculate and display percentage change with a green or red arrow. A custom fl_chart line chart shows the trend. A ChoiceChips filter switches between Today, Week, and Month date ranges, re-querying both the cards and the chart.

Prerequisites

  • A FlutterFlow project with Firebase/Firestore connected
  • Firebase Blaze plan enabled (Cloud Functions require Blaze)
  • FlutterFlow Pro plan for Custom Widgets and Custom Functions
  • Basic understanding of Backend Queries and Component Parameters

Step-by-step guide

1

Create the analytics_daily Firestore collection and Cloud Function aggregation pattern

In Firestore, create a collection called analytics_daily. Each document represents one day's aggregated metrics. Document ID format: YYYY-MM-DD (e.g., 2026-03-29). Fields: date (Timestamp), totalUsers (Integer — unique users that day), newUsers (Integer — first-time users), revenue (Double — total revenue in dollars), activeSessions (Integer — sessions with at least one interaction). Manually create 7-14 sample documents with realistic data for development. For production, deploy a scheduled Cloud Function (exports.aggregateDaily = functions.pubsub.schedule('every 24 hours').onRun()) that queries your raw events collection, computes the five metrics, and writes one analytics_daily document. This pattern keeps your dashboard queries to a single document read per day instead of scanning thousands of raw events.

Expected result: The analytics_daily collection exists in Firestore with sample documents. Each document has date, totalUsers, newUsers, revenue, and activeSessions fields with realistic test data.

2

Build the KPI metric card Component with percentage change indicator

Create a new Component called KPIMetricCard. Add four Component Parameters: label (String), currentValue (Double), previousValue (Double), and iconName (String). Layout: outer Container with 16px padding, white background, 12px borderRadius, and a subtle boxShadow. Inside, a Column with crossAxisAlignment start. First child: an Icon widget bound to iconName parameter (use Conditional Value to map strings to Icons — 'users' to Icons.people, 'revenue' to Icons.attach_money, 'sessions' to Icons.trending_up). Second child: a Text widget showing currentValue formatted appropriately (use Custom Function formatMetric that returns '1.2K' for thousands, '$12,450' for revenue). Set style to headlineLarge, fontWeight bold. Third child: a Text widget showing the label parameter in bodyMedium, secondary color. Fourth child: a Row with an Icon (Icons.arrow_upward in green or Icons.arrow_downward in red — Conditional Value based on whether percentage is positive) and a Text widget showing the percentage. Create a Custom Function called calcPercentChange with parameters current (Double) and previous (Double) that returns ((current - previous) / previous * 100).toStringAsFixed(1). Use Conditional Styles: green (#4CAF50) text when positive, red (#F44336) when negative.

custom_functions.dart
1// Custom Function: calcPercentChange
2// Return Type: String
3// Parameters: current (Double), previous (Double)
4
5String calcPercentChange(double current, double previous) {
6 if (previous == 0) return '0.0';
7 final change = ((current - previous) / previous) * 100;
8 return change.toStringAsFixed(1);
9}
10
11// Custom Function: formatMetric
12// Return Type: String
13// Parameters: value (Double), prefix (String, optional)
14
15String formatMetric(double value, {String prefix = ''}) {
16 if (value >= 1000000) {
17 return '$prefix${(value / 1000000).toStringAsFixed(1)}M';
18 } else if (value >= 1000) {
19 return '$prefix${(value / 1000).toStringAsFixed(1)}K';
20 }
21 return '$prefix${value.toStringAsFixed(0)}';
22}

Expected result: A reusable KPIMetricCard Component that accepts label, currentValue, previousValue, and iconName. It displays the formatted value, label, and a green up-arrow or red down-arrow with percentage change.

3

Place KPI cards in a 2-column GridView on the dashboard page

Create a new page called AnalyticsDashboard. Add a Backend Query at the page level: query analytics_daily, ordered by date descending, limit 2 (today + yesterday for comparison). Bind the query result to Page State variable analyticsData. Below the AppBar, add a Padding (16px all sides) wrapping a GridView with crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 12, and childAspectRatio: 1.4. Place four KPIMetricCard Components inside the GridView. Bind each card's parameters: first card — label: 'Total Users', currentValue: analyticsData[0].totalUsers, previousValue: analyticsData[1].totalUsers, iconName: 'users'. Second card — label: 'Revenue', currentValue: analyticsData[0].revenue, previousValue: analyticsData[1].revenue, iconName: 'revenue'. Third card — label: 'New Users', currentValue: analyticsData[0].newUsers, previousValue: analyticsData[1].newUsers, iconName: 'users'. Fourth card — label: 'Sessions', currentValue: analyticsData[0].activeSessions, previousValue: analyticsData[1].activeSessions, iconName: 'sessions'.

Expected result: The dashboard page shows four KPI cards in a 2-column grid. Each card displays a metric from today's analytics_daily document with percentage change compared to yesterday.

4

Build a trend line chart Custom Widget using fl_chart LineChart

Add fl_chart to your project dependencies (pubspec.yaml via Custom Code settings: fl_chart: ^0.68.0). Create a Custom Widget called TrendLineChart with parameters: dataPoints (List<JSON> — each item has 'x' as double index and 'y' as double value), lineColor (Color, default blue), and chartHeight (Double, default 200). In the widget build method, return a Container with a FIXED height set to chartHeight (critical — fl_chart crashes without a bounded height). Inside, place a LineChart widget. Configure LineChartData: gridData with show: false, titlesData showing only bottom axis labels (dates) and left axis values, borderData with show: false. Create one LineChartBarData with spots mapped from dataPoints as FlSpot(item['x'], item['y']), isCurved: true, color: lineColor, barWidth: 3, dotData: FlDotData(show: false), belowBarData: BarAreaData(show: true, color: lineColor.withOpacity(0.1)). Enable touch tooltips: lineTouchData with LineTouchTooltipData showing the y value on hover. On the dashboard page, add a Backend Query for analytics_daily ordered by date ascending for the selected range, map results to the dataPoints format using a Custom Function, and pass them to TrendLineChart.

trend_line_chart.dart
1// Custom Widget: TrendLineChart
2import 'package:fl_chart/fl_chart.dart';
3
4class TrendLineChart extends StatelessWidget {
5 final List<dynamic> dataPoints;
6 final Color lineColor;
7 final double chartHeight;
8
9 const TrendLineChart({
10 super.key,
11 required this.dataPoints,
12 this.lineColor = const Color(0xFF2196F3),
13 this.chartHeight = 200,
14 });
15
16 @override
17 Widget build(BuildContext context) {
18 final spots = dataPoints.asMap().entries.map((e) {
19 return FlSpot(e.key.toDouble(), (e.value['y'] as num).toDouble());
20 }).toList();
21
22 return SizedBox(
23 height: chartHeight,
24 child: LineChart(
25 LineChartData(
26 gridData: const FlGridData(show: false),
27 titlesData: const FlTitlesData(
28 topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
29 rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
30 bottomTitles: AxisTitles(
31 sideTitles: SideTitles(showTitles: true, reservedSize: 30),
32 ),
33 leftTitles: AxisTitles(
34 sideTitles: SideTitles(showTitles: true, reservedSize: 40),
35 ),
36 ),
37 borderData: FlBorderData(show: false),
38 lineTouchData: LineTouchData(
39 touchTooltipData: LineTouchTooltipData(
40 getTooltipItems: (spots) => spots.map((s) {
41 return LineTooltipItem(
42 s.y.toStringAsFixed(0),
43 const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
44 );
45 }).toList(),
46 ),
47 ),
48 lineBarsData: [
49 LineChartBarData(
50 spots: spots,
51 isCurved: true,
52 color: lineColor,
53 barWidth: 3,
54 dotData: const FlDotData(show: false),
55 belowBarData: BarAreaData(
56 show: true,
57 color: lineColor.withOpacity(0.1),
58 ),
59 ),
60 ],
61 ),
62 ),
63 );
64 }
65}

Expected result: A smooth curved line chart renders on the dashboard below the KPI cards, showing the trend of a selected metric over the date range. Touch tooltips display exact values.

5

Add date range filter with ChoiceChips that re-queries KPI cards and chart

Above the KPI GridView, add a Row with a ChoiceChips widget. Options: 'Today', '7 Days', '30 Days'. Bind the selected value to a Page State variable called selectedRange (String, default '7 Days'). On the ChoiceChips On Changed action: first, update Page State selectedRange to the new value. Then use a Custom Action or conditional logic to compute the start date: 'Today' = start of today, '7 Days' = 7 days ago, '30 Days' = 30 days ago. Update a Page State variable filterStartDate (DateTime). Both the KPI card Backend Query and the chart Backend Query should use filterStartDate as the where clause: date >= filterStartDate. For KPI comparison: query the two most recent documents within the range for the card values, and query the document just before the range start for the previousValue. When selectedRange changes, both queries automatically re-execute because they reference the Page State variable. The chart query fetches all documents within the range ordered by date ascending.

Expected result: Tapping a ChoiceChip (Today, 7 Days, 30 Days) immediately refreshes both the KPI metric cards and the trend line chart with data from the selected date range.

Complete working example

Analytics Dashboard Architecture
1Firestore Data Model:
2 analytics_daily/{YYYY-MM-DD}
3 date: Timestamp (2026-03-29)
4 totalUsers: Integer (1523)
5 newUsers: Integer (87)
6 revenue: Double (12450.00)
7 activeSessions: Integer (892)
8
9Cloud Function (scheduled daily):
10 exports.aggregateDaily = functions.pubsub
11 .schedule('every 24 hours').onRun(async () => {
12 const today = new Date(); today.setHours(0,0,0,0);
13 const tomorrow = new Date(today); tomorrow.setDate(today.getDate()+1);
14 const events = await db.collection('raw_events')
15 .where('timestamp','>=',today).where('timestamp','<',tomorrow).get();
16 const uniqueUsers = new Set(events.docs.map(d => d.data().userId)).size;
17 const newUsers = /* count first-seen users */;
18 const revenue = events.docs.reduce((s,d) => s + (d.data().amount||0), 0);
19 const sessions = events.docs.filter(d => d.data().type==='session').length;
20 await db.collection('analytics_daily').doc(formatDate(today)).set({
21 date: admin.firestore.Timestamp.fromDate(today),
22 totalUsers: uniqueUsers, newUsers, revenue, activeSessions: sessions
23 });
24 });
25
26Custom Functions:
27 calcPercentChange(current, previous) String percentage
28 formatMetric(value, prefix) String formatted number
29
30Dashboard Page:
31 AppBar (title: "Analytics")
32 Padding (16px)
33 ChoiceChips [Today | 7 Days | 30 Days]
34 On Changed Update Page State: selectedRange, filterStartDate
35 SizedBox (h: 16)
36 GridView (crossAxisCount: 2, spacing: 12)
37 KPIMetricCard (label: Total Users, icon: people)
38 KPIMetricCard (label: Revenue, icon: attach_money)
39 KPIMetricCard (label: New Users, icon: person_add)
40 KPIMetricCard (label: Sessions, icon: trending_up)
41 SizedBox (h: 24)
42 TrendLineChart (Custom Widget, fl_chart)
43 SizedBox height: 200 (REQUIRED fl_chart needs bounded height)
44 Backend Queries:
45 KPI Query: analytics_daily, orderBy date DESC, limit 2, where date >= filterStartDate
46 Chart Query: analytics_daily, orderBy date ASC, where date >= filterStartDate
47
48KPIMetricCard Component:
49 Parameters: label (String), currentValue (Double), previousValue (Double), iconName (String)
50 Container (white, rounded, shadow)
51 Column
52 Icon (mapped from iconName)
53 Text (formatMetric(currentValue), headlineLarge, bold)
54 Text (label, bodyMedium, secondary)
55 Row
56 Icon (arrow_upward green | arrow_downward red)
57 Text (calcPercentChange(current, previous) + '%', green | red)

Common mistakes when creating a Custom Analytics Widget for Your FlutterFlow App

Why it's a problem: Running aggregation queries across raw event data in real-time

How to avoid: Pre-aggregate with a scheduled Cloud Function that writes one analytics_daily document per day. Dashboard queries read 7-30 small documents instead of thousands of raw events.

Why it's a problem: Not setting a fixed height on the fl_chart container

How to avoid: Always wrap the LineChart Custom Widget in a SizedBox or Container with an explicit height value (e.g., 200). Never rely on Expanded or flexible sizing alone for the chart container.

Why it's a problem: Showing stale KPI data after changing the date filter

How to avoid: Ensure both the chart Backend Query and the KPI card Backend Query use filterStartDate in their where clause. When the Page State variable changes, both queries re-execute automatically.

Best practices

  • Pre-aggregate analytics data with Cloud Functions instead of computing metrics from raw events at query time
  • Use Component Parameters for all dynamic values in KPIMetricCard so it is fully reusable across different metrics
  • Always set a fixed height on fl_chart containers — use SizedBox with an explicit height, never unbounded constraints
  • Format large numbers with K/M suffixes (1,523 → '1.5K') using a Custom Function for readability on small cards
  • Cache the previous period value alongside the current period in your query to avoid a separate Firestore read for comparison data
  • Use Conditional Styles (green/red) for percentage change indicators rather than separate widgets to keep the widget tree clean
  • Test the dashboard with empty analytics_daily data and add an empty state Component so new apps show a helpful message instead of a blank screen

Still stuck?

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

ChatGPT Prompt

Design a Firestore data model for a pre-aggregated analytics dashboard. I need a collection called analytics_daily where each document has date, totalUsers, newUsers, revenue, and activeSessions. Give me a scheduled Cloud Function that aggregates raw event data into this collection daily, and a Dart function that calculates percentage change between two values.

FlutterFlow Prompt

Create a dashboard page with a 2-column GridView containing four KPI metric cards. Each card should show an icon, a large number, a label, and a percentage change with a green up-arrow or red down-arrow. Below the grid, add a line chart showing a trend over 7 days. Add ChoiceChips at the top for Today, 7 Days, and 30 Days date filtering.

Frequently asked questions

How often should the Cloud Function aggregate analytics data?

For most apps, once daily (scheduled at midnight UTC) is sufficient. If your dashboard needs fresher data, run the function hourly. Avoid running more frequently than hourly — the cost savings of pre-aggregation diminish if you re-aggregate constantly. Store a lastAggregated timestamp in each document so the dashboard can show data freshness.

Can I use Supabase instead of Firestore for the analytics backend?

Yes, and SQL-based aggregation is actually easier for analytics. Use a Supabase SQL view or database function with GROUP BY date to compute daily totals. Query the view directly from FlutterFlow with a Supabase Backend Query. You skip the Cloud Function entirely because PostgreSQL handles aggregation natively.

How do I calculate percentage change between periods?

Create a Custom Function with parameters current (Double) and previous (Double). Formula: ((current - previous) / previous) * 100. Handle the edge case where previous is zero to avoid division by zero — return 0 or 'N/A'. Display the result with toStringAsFixed(1) for one decimal place (e.g., '+12.3%').

Why does my fl_chart LineChart show a blank space or crash?

fl_chart requires a bounded height constraint. If the chart is inside a Column or ListView without a fixed-height wrapper, Flutter cannot determine the chart dimensions and either renders nothing or throws a RenderBox layout error. Wrap your TrendLineChart Custom Widget in a SizedBox with an explicit height (e.g., 200).

How do I add more metrics to the dashboard later?

Add new fields to the analytics_daily documents (e.g., churnRate, avgSessionDuration) and update the Cloud Function to compute them. Then drop another KPIMetricCard Component into the GridView with the new field bindings. Because the card is parameterized, no new Component is needed — just a new instance with different parameters.

Can RapidDev help build a production analytics dashboard?

Yes. A production analytics dashboard often requires custom Cloud Functions for multi-source aggregation, real-time streaming for live metrics, role-based access for team dashboards, and export functionality. RapidDev can architect the full data pipeline and FlutterFlow UI beyond what the visual builder handles alone.

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.