Build a business KPI tracking system in FlutterFlow for metrics like revenue, churn rate, and active users. You will create a metrics Firestore collection with current value, previous value, and targets, a metric_history subcollection for trend data, KPI cards with trend arrows and target progress bars, sparkline Custom Widgets showing recent history, a manual update form for metrics not connected to APIs, and a Cloud Function that checks thresholds and sends alert notifications when metrics cross critical boundaries.
Track business KPIs with trend indicators, sparklines, and threshold alerts
This tutorial builds a business metrics tracking system for non-technical founders who need to monitor key performance indicators like monthly revenue, churn rate, NPS score, and active users. Unlike app analytics that track user behavior events, this system tracks business-level KPIs that may come from manual entry, external APIs, or Firestore aggregation queries. Each metric displays its current value, comparison to the previous period with a trend arrow, progress toward a defined target, and a sparkline showing recent history. Cloud Functions monitor metrics against thresholds and send push notifications when a metric crosses a critical boundary.
Prerequisites
- A FlutterFlow project with Firebase/Firestore connected
- Firebase Blaze plan for Cloud Functions
- FlutterFlow Pro plan for Custom Widgets
- Basic understanding of Firestore documents and Backend Queries
Step-by-step guide
Design the metrics and metric_history Firestore collections
Design the metrics and metric_history Firestore collections
Create a collection called metrics. Each document represents one business KPI. Fields: name (String, e.g., 'Monthly Revenue'), type (String enum: counter, gauge, percentage, currency), currentValue (Double), previousValue (Double), target (Double), unit (String, e.g., '$', '%', 'users'), category (String, e.g., 'Financial', 'Growth', 'Engagement'), lastUpdated (Timestamp), thresholdHigh (Double, optional), thresholdLow (Double, optional). Create a subcollection called metric_history under each metric document with fields: value (Double), timestamp (Timestamp). Add 5-6 sample metrics: Monthly Revenue ($45,000 target $50,000), Active Users (1,250 target 2,000), Churn Rate (4.2% threshold 5%), NPS Score (72 target 80), Weekly Signups (89 target 100), Support Tickets Open (23 threshold 30). For each metric, add 12-16 history documents representing the past 4 months of weekly snapshots.
Expected result: The metrics collection contains 5-6 sample KPIs with current and target values. Each metric has a metric_history subcollection with weekly historical data points.
Build the KPI metric card Component with trend arrow and target progress bar
Build the KPI metric card Component with trend arrow and target progress bar
Create a Component called MetricCard with Parameters: metricName (String), currentValue (Double), previousValue (Double), target (Double), unit (String), type (String). Layout: Container with white background, 16px padding, 12px borderRadius, and a subtle shadow. Inside, a Column with crossAxisAlignment start. First child: a Text widget showing metricName in bodyMedium with secondary color. Second child: a Row with the formatted current value in headlineMedium bold (use a Custom Function formatByType that formats currency with '$' prefix and commas, percentages with '%' suffix, and plain numbers with K/M suffixes) and a trend indicator — an Icon (Icons.trending_up in green or Icons.trending_down in red) based on whether currentValue is greater than previousValue, plus a Text showing the percentage change. Third child: a LinearProgressIndicator showing progress toward the target. Set value to (currentValue / target).clamp(0.0, 1.0) using a Custom Function. Set the color to green if above 80% of target, amber if 50-80%, red if below 50%. Fourth child: a Text showing 'Target: {formatted target}' in caption style.
1// Custom Function: formatByType2// Return Type: String3// Parameters: value (Double), type (String), unit (String)45String formatByType(double value, String type, String unit) {6 switch (type) {7 case 'currency':8 if (value >= 1000000) return '$unit${(value / 1000000).toStringAsFixed(1)}M';9 if (value >= 1000) return '$unit${(value / 1000).toStringAsFixed(1)}K';10 return '$unit${value.toStringAsFixed(0)}';11 case 'percentage':12 return '${value.toStringAsFixed(1)}$unit';13 default:14 if (value >= 1000) return '${(value / 1000).toStringAsFixed(1)}K';15 return value.toStringAsFixed(0);16 }17}1819// Custom Function: calcProgress20// Return Type: Double21// Parameters: current (Double), target (Double)2223double calcProgress(double current, double target) {24 if (target == 0) return 0.0;25 return (current / target).clamp(0.0, 1.0);26}Expected result: A MetricCard Component displays the metric name, formatted value with trend arrow, and a color-coded progress bar toward the target.
Create a sparkline Custom Widget for metric history visualization
Create a sparkline Custom Widget for metric history visualization
Add fl_chart to your project dependencies (fl_chart: ^0.68.0 in Custom Code settings). Create a Custom Widget called SparklineChart with parameters: dataPoints (List<JSON>, each with value as Double), lineColor (Color, default blue), height (Double, default 40). The widget renders a compact, minimal line chart with no axes, no grid, and no labels — just the trend line. Configure LineChartData with gridData show false, titlesData all hidden, borderData show false, and lineTouchData enabled false. Map dataPoints to FlSpot using the list index as x and the value as y. Set isCurved true, barWidth 2, dotData show false, and belowBarData with a subtle gradient fill. Wrap everything in a SizedBox with the height parameter. On the MetricCard Component, add the SparklineChart below the progress bar. Query the metric_history subcollection ordered by timestamp descending, limit 12, and pass the results as dataPoints. Set lineColor to match the trend direction: green if the latest value is above the earliest, red if below.
1// Custom Widget: SparklineChart2import 'package:fl_chart/fl_chart.dart';34class SparklineChart extends StatelessWidget {5 final List<dynamic> dataPoints;6 final Color lineColor;7 final double height;89 const SparklineChart({10 super.key,11 required this.dataPoints,12 this.lineColor = const Color(0xFF2196F3),13 this.height = 40,14 });1516 @override17 Widget build(BuildContext context) {18 final spots = dataPoints.asMap().entries.map((e) {19 return FlSpot(20 e.key.toDouble(),21 (e.value['value'] as num).toDouble(),22 );23 }).toList();2425 return SizedBox(26 height: height,27 child: LineChart(28 LineChartData(29 gridData: const FlGridData(show: false),30 titlesData: const FlTitlesData(show: false),31 borderData: FlBorderData(show: false),32 lineTouchData: const LineTouchData(enabled: false),33 lineBarsData: [34 LineChartBarData(35 spots: spots,36 isCurved: true,37 color: lineColor,38 barWidth: 2,39 dotData: const FlDotData(show: false),40 belowBarData: BarAreaData(41 show: true,42 color: lineColor.withOpacity(0.1),43 ),44 ),45 ],46 ),47 ),48 );49 }50}Expected result: Each MetricCard shows a compact sparkline below the progress bar, visualizing the metric's trend over recent history without taking up much vertical space.
Build the metrics dashboard page with category filtering
Build the metrics dashboard page with category filtering
Create a page called MetricsDashboard. Add a Backend Query at page level that fetches all documents from the metrics collection ordered by category ascending. Below the AppBar, add a ChoiceChips widget with options: All, Financial, Growth, Engagement. Bind the selected value to a Page State variable selectedCategory (String, default 'All'). Below the ChoiceChips, add a GridView with crossAxisCount 2, spacing 12, and childAspectRatio 0.85. Generate children from the Backend Query results filtered by selectedCategory (or show all if 'All' is selected). Each child is a MetricCard Component with parameters bound to the document fields. Add a FloatingActionButton with an Icons.edit icon. On tap, navigate to a MetricUpdatePage where admins can select a metric from a DropDown, enter a new value in a TextField, and tap Update. The Update action writes the new value to the metric document (updating currentValue, setting previousValue to the old currentValue, and updating lastUpdated) and also adds a document to the metric_history subcollection.
Expected result: The dashboard shows all business KPIs in a filterable grid. Tapping the edit button navigates to a form where admins can manually update metric values.
Set up threshold alerts via Cloud Function notifications
Set up threshold alerts via Cloud Function notifications
Deploy a Cloud Function called checkMetricThresholds triggered by Firestore document writes on the metrics collection (functions.firestore.document('metrics/{metricId}').onWrite). When a metric document is updated, the function reads the new currentValue and compares it against thresholdHigh and thresholdLow. If currentValue exceeds thresholdHigh or drops below thresholdLow, the function sends a push notification to admin users via Firebase Cloud Messaging. The notification title is 'Metric Alert: {metricName}' and the body describes the threshold breach. In FlutterFlow, configure push notifications in the Settings panel. Create an admin_users collection or use a Firestore field isAdmin on user documents to determine who receives alerts. Additionally, log each alert to a metric_alerts collection with metricId, alertType (high or low), triggeredValue, threshold, and timestamp for audit history.
Expected result: When a metric value crosses a defined threshold, admin users receive a push notification alerting them to the breach. All alerts are logged in the metric_alerts collection.
Add a weekly digest summary with period-over-period comparison
Add a weekly digest summary with period-over-period comparison
Deploy a scheduled Cloud Function called weeklyDigest that runs every Monday at 8 AM via functions.pubsub.schedule. The function reads all metrics documents, fetches the metric_history entry from 7 days ago for each metric, calculates week-over-week change, and compiles a summary. Format the summary as an HTML email using a template: metric name, current value, previous week value, change percentage with green/red color coding, and target progress. Send the email via a configured email service (SendGrid, Mailgun, or Firebase Extensions email). In FlutterFlow, add a DigestPreview page that shows the same weekly summary in-app. Use a ListView of MetricSummaryRow Components, each showing the metric name, current vs previous value, change arrow, and a mini progress bar. This page queries the same data the Cloud Function uses, giving admins an in-app preview of what the email contains.
Expected result: Admin users receive a weekly email digest every Monday summarizing all KPI changes. An in-app preview page shows the same weekly comparison data.
Complete working example
1Firestore Data Model:2├── metrics/{auto-id}3│ ├── name: String (Monthly Revenue)4│ ├── type: String (counter | gauge | percentage | currency)5│ ├── currentValue: Double (45000)6│ ├── previousValue: Double (42000)7│ ├── target: Double (50000)8│ ├── unit: String ($ | % | users)9│ ├── category: String (Financial | Growth | Engagement)10│ ├── lastUpdated: Timestamp11│ ├── thresholdHigh: Double (optional)12│ ├── thresholdLow: Double (optional)13│ └── metric_history/{auto-id}14│ ├── value: Double15│ └── timestamp: Timestamp16├── metric_alerts/{auto-id}17│ ├── metricId: String18│ ├── alertType: String (high | low)19│ ├── triggeredValue: Double20│ ├── threshold: Double21│ └── timestamp: Timestamp2223Cloud Functions:24 checkMetricThresholds — onWrite metrics/{id}, send FCM if threshold crossed25 weeklyDigest — every Monday 8 AM, email summary of all KPIs2627Custom Functions:28 formatByType(value, type, unit) → formatted string29 calcProgress(current, target) → double 0.0-1.030 calcPercentChange(current, previous) → string percentage3132Custom Widget:33 SparklineChart — fl_chart LineChart, no axes, compact 40px height3435Page: MetricsDashboard36├── AppBar (title: Business Metrics)37├── ChoiceChips [All | Financial | Growth | Engagement]38│ └── On Changed → Update Page State: selectedCategory39├── GridView (crossAxisCount: 2, aspect: 0.85)40│ └── MetricCard Component (repeated, filtered by category)41│ ├── Text (metricName, bodyMedium)42│ ├── Row: formatted value (headlineMedium) + trend Icon + % change43│ ├── LinearProgressIndicator (currentValue / target, color-coded)44│ ├── Text (Target: formatted target)45│ └── SparklineChart (metric_history, 12 points, height: 40)46├── FloatingActionButton → Navigate to MetricUpdatePage47└── Backend Query: metrics, orderBy category ASC4849Page: MetricUpdatePage50├── DropDown (select metric)51├── TextField (new value)52└── Button (Update → write currentValue, previousValue, metric_history)5354Page: DigestPreview55└── ListView of MetricSummaryRow Components56 └── Row: name + current + previous + change % + mini progress barCommon mistakes
Why it's a problem: Showing only the current metric value without historical context
How to avoid: Always show three pieces of context alongside the current value: previous period comparison with a trend arrow, a sparkline of recent history, and progress toward the defined target.
Why it's a problem: Storing metric history in the same document as the current value
How to avoid: Use a metric_history subcollection under each metric document. Each history entry is its own lightweight document. Query with limit and orderBy to fetch only the data points you need for sparklines.
Why it's a problem: Not handling the division-by-zero case in percentage change calculations
How to avoid: In your calcPercentChange Custom Function, check if previousValue equals zero before dividing. Return '0.0' or 'New' as a string when there is no previous value to compare against.
Best practices
- Always display current value, trend direction, target progress, and sparkline history together on each metric card for full context
- Use a metric_history subcollection instead of an array field to avoid Firestore document size limits
- Color-code progress bars by percentage of target achieved: green above 80%, amber 50-80%, red below 50%
- Set meaningful thresholds for automated alerts — only alert on metrics that require immediate action to avoid notification fatigue
- Use the formatByType Custom Function to handle currency, percentage, and plain number formatting consistently across all cards
- Add pull-to-refresh on the dashboard so admins can manually refresh after updating a metric
- Log all threshold alerts to a metric_alerts collection for audit history and trend analysis of alert frequency
- Start with manual metric updates and add API-based automation incrementally as data sources are identified
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Design a Firestore data model for a business KPI tracking system. I need a metrics collection with fields for name, type (counter/gauge/percentage/currency), currentValue, previousValue, target, unit, category, and thresholds. Include a metric_history subcollection for trend data. Write Dart functions for formatting values by type (currency with $ and commas, percentages with %, numbers with K/M) and calculating progress toward a target as a 0-1 double.
Create a business metrics dashboard page with a ChoiceChips filter for categories (All, Financial, Growth, Engagement) and a 2-column GridView of metric cards. Each card should display the metric name, a large formatted value, a trend arrow showing up or down versus the previous period, a color-coded progress bar toward a target value, and a small sparkline chart showing recent history. Add a FloatingActionButton that navigates to a metric update form.
Frequently asked questions
How is this different from the analytics platform tutorial?
The analytics platform tutorial tracks user behavior events (page views, clicks, signups) with funnels and cohorts. This metrics tracking system tracks business-level KPIs like revenue, churn rate, and NPS that may come from manual entry, external APIs, or aggregated Firestore queries. Think of analytics as what users do, and metrics as how the business performs.
Can metrics update automatically from external APIs?
Yes. Deploy a scheduled Cloud Function that calls external APIs (Stripe for revenue, your auth system for active users, a support tool for ticket counts) and updates the corresponding metric document in Firestore. Start with manual updates to validate the dashboard, then automate one metric at a time.
How many data points should the sparkline show?
12-16 data points work well for a compact sparkline at 40px height. For weekly snapshots, this covers 3-4 months. For daily snapshots, 14 data points covers two weeks. Query metric_history ordered by timestamp descending with a limit of 12-16 to keep the widget lightweight.
What happens if a metric does not have a target value?
Set the target to zero or null and use Conditional Visibility to hide the progress bar on the MetricCard Component when target is zero. The card still shows the current value, trend arrow, and sparkline. Not every metric needs a target — some like churn rate are better tracked with thresholds than targets.
How do I add a new metric to the tracking system?
Add a new document to the metrics collection with the metric name, type, initial currentValue, target, unit, and category. The dashboard GridView automatically picks it up on the next query. No code changes or redeployment needed — this is the advantage of a data-driven approach where the UI renders whatever metrics exist in Firestore.
Can RapidDev help connect metrics to live data sources?
Yes. Connecting business metrics to live data from Stripe, Google Analytics, CRM systems, and internal databases often requires custom Cloud Functions, API authentication, data transformation, and error handling. RapidDev can build the integration layer that keeps your FlutterFlow metrics dashboard updated automatically from all your business tools.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation