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

How to Create a Time Tracking Tool in FlutterFlow

Create a time tracking app in FlutterFlow by storing projects and time entries in Firestore, managing active timer state with Page State variables, and using a Custom Function to calculate elapsed seconds. A periodic action fires every second to refresh the display. History groups entries by date, and a Cloud Function generates CSV reports for export.

What you'll learn

  • Set up Firestore collections for projects and time entries
  • Implement a live countdown timer using Page State and a periodic action
  • Group time entry history by date in a ListView
  • Export time reports as CSV via a Cloud Function
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read45-60 minFlutterFlow Pro+ (code export required for Cloud Function CSV export)March 2026RapidDev Engineering Team
TL;DR

Create a time tracking app in FlutterFlow by storing projects and time entries in Firestore, managing active timer state with Page State variables, and using a Custom Function to calculate elapsed seconds. A periodic action fires every second to refresh the display. History groups entries by date, and a Cloud Function generates CSV reports for export.

Timer-Based Work Tracking with Firestore

This tutorial walks you through building a full time tracking tool inside FlutterFlow. You will create two Firestore collections — projects and time_entries — and wire up a start/stop timer on the main screen. The active timer lives in Page State (startTime + isRunning), and a Custom Function recalculates elapsed time every second via a periodic action. A project DropDown lets users assign each entry to a project. The history screen shows all entries grouped by date in a ListView. Finally, a Cloud Function generates a CSV file stored in Firebase Storage, which the app downloads via Launch URL.

Prerequisites

  • FlutterFlow Pro account with a Firebase project connected
  • Firestore database enabled in the Firebase Console
  • Basic familiarity with FlutterFlow's widget tree and Actions panel
  • Firebase Storage enabled (for CSV export step)

Step-by-step guide

1

Create Firestore Collections for Projects and Time Entries

In the Firebase Console, open Firestore Database and create two collections. The first collection is named projects with fields: name (String), color (String), userId (String). The second collection is named time_entries with fields: projectId (String), projectName (String), startTime (Timestamp), endTime (Timestamp), durationSeconds (Integer), userId (String), date (String — store as YYYY-MM-DD for easy grouping). Back in FlutterFlow, open the Firestore panel, click the refresh icon to import your schema, and create matching Document Types. Set up a Collection Group query for time_entries filtered by userId equals currentUser.uid so each user only sees their own data.

Expected result: Both collections appear in FlutterFlow's Firestore panel with correct field types.

2

Build the Timer Screen with Page State Variables

Open your main Timer page and click the gear icon to open Page State. Add three variables: isRunning (Boolean, default false), startTime (DateTime, no default), and elapsedSeconds (Integer, default 0). Add a Text widget at the top of the page and bind its value to a Custom Function called formatElapsed that accepts elapsedSeconds as an Integer and returns a formatted String like 02:34:17. Below the timer display, add a DropDown widget bound to a Backend Query that streams your projects collection filtered by userId. Store the selected project in a fourth Page State variable selectedProjectId (String). Add two buttons — Start and Stop — side by side at the bottom of the timer section.

format_elapsed.dart
1// Custom Function: formatElapsed
2// Argument: elapsedSeconds (int)
3// Return type: String
4String formatElapsed(int elapsedSeconds) {
5 final hours = elapsedSeconds ~/ 3600;
6 final minutes = (elapsedSeconds % 3600) ~/ 60;
7 final seconds = elapsedSeconds % 60;
8 return '${hours.toString().padLeft(2, '0')}:'
9 '${minutes.toString().padLeft(2, '0')}:'
10 '${seconds.toString().padLeft(2, '0')}'
11;}

Expected result: The timer page shows 00:00:00, a project picker DropDown, and Start/Stop buttons.

3

Wire Up the Start and Stop Timer Actions

Select the Start button and open its On Tap action chain. Add three actions in order: first, Update Page State — set isRunning to true and set startTime to Current Time. Second, add a Timer action (found under Utilities) — set it to repeat every 1000 milliseconds (1 second) and choose Do Not Stop Automatically. Inside the Timer's action, add Update Page State to increment elapsedSeconds by 1. Third, conditionally show/hide the Start and Stop buttons based on the isRunning Page State variable using Conditional Visibility on each button. Select the Stop button and add its On Tap action chain: Update Page State — set isRunning to false. Then add a Firestore Create Document action targeting the time_entries collection. Map the fields: projectId from selectedProjectId, startTime from the startTime Page State, endTime to Current Time, durationSeconds from elapsedSeconds, date formatted as YYYY-MM-DD using a Custom Function, userId from currentUser.uid. Finally, reset elapsedSeconds to 0.

Expected result: Tapping Start increments the timer every second. Tapping Stop saves the entry to Firestore and resets the display.

4

Build the History Screen Grouped by Date

Create a new page called History. Add a Backend Query at the page level: query the time_entries collection, filter by userId equals currentUser.uid, and order by date descending. Add a ListView widget on the page bound to this query. Inside the ListView, add a Column with two sections: a date header Text widget bound to the entry's date field, and an entry row showing the projectName, formatted duration (reuse your formatElapsed Custom Function), and start/end times. To visually group by date, use a Conditional Visibility condition on the date header Text widget — show it only when the current item's date differs from the previous item's date. You can achieve this by comparing the current index's date field with the previous index using a Custom Function called isNewDate that returns true when dates differ.

Expected result: The History page displays entries with a bold date header appearing once per day group.

5

Generate and Download a CSV Report via Cloud Function

In the Firebase Console, create a Cloud Function (Node.js) named generateTimeReport. The function accepts a userId and optional dateRange, queries time_entries for that user, formats the data as CSV, uploads the file to Firebase Storage under reports/{userId}/time_report.csv, generates a signed download URL, and returns the URL in the response. Back in FlutterFlow, open the API Calls panel and create a new API call — Cloud Function type — pointing to your function's HTTPS endpoint. Pass currentUser.uid as the userId parameter. On the Reports page, add a button labeled Download CSV. In its On Tap action, call the API, then use the Launch URL action with the returned download URL. A file manager or browser will open for the user to save the file.

index.js
1// Cloud Function: generateTimeReport (index.js)
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4admin.initializeApp();
5
6exports.generateTimeReport = functions.https.onCall(async (data, context) => {
7 if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
8 const userId = context.auth.uid;
9 const db = admin.firestore();
10 const snapshot = await db.collection('time_entries')
11 .where('userId', '==', userId)
12 .orderBy('date', 'desc')
13 .limit(1000)
14 .get();
15
16 const rows = ['Date,Project,Start,End,Duration (min)'];
17 snapshot.forEach(doc => {
18 const d = doc.data();
19 const dur = Math.round((d.durationSeconds || 0) / 60);
20 rows.push(`${d.date},${d.projectName},${d.startTime.toDate().toISOString()},${d.endTime.toDate().toISOString()},${dur}`);
21 });
22
23 const csv = rows.join('\n');
24 const bucket = admin.storage().bucket();
25 const file = bucket.file(`reports/${userId}/time_report.csv`);
26 await file.save(csv, { contentType: 'text/csv' });
27 const [url] = await file.getSignedUrl({ action: 'read', expires: Date.now() + 15 * 60 * 1000 });
28 return { url };
29});

Expected result: Tapping Download CSV triggers the Cloud Function, and the device opens a file download link for the generated CSV.

Complete working example

format_elapsed.dart
1// Custom Function: formatElapsed
2// Place in FlutterFlow > Custom Functions
3// Arguments: elapsedSeconds (int)
4// Return type: String
5
6String formatElapsed(int elapsedSeconds) {
7 final hours = elapsedSeconds ~/ 3600;
8 final minutes = (elapsedSeconds % 3600) ~/ 60;
9 final seconds = elapsedSeconds % 60;
10 return '${hours.toString().padLeft(2, '0')}:'
11 '${minutes.toString().padLeft(2, '0')}:'
12 '${seconds.toString().padLeft(2, '0')}';
13}
14
15// Custom Function: formatDateLabel
16// Arguments: isoDate (String)
17// Return type: String
18String formatDateLabel(String isoDate) {
19 try {
20 final dt = DateTime.parse(isoDate);
21 const months = [
22 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
23 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
24 ];
25 return '${months[dt.month - 1]} ${dt.day}, ${dt.year}';
26 } catch (_) {
27 return isoDate;
28 }
29}
30
31// Custom Function: isNewDate
32// Arguments: currentDate (String), previousDate (String)
33// Return type: bool
34bool isNewDate(String currentDate, String previousDate) {
35 return currentDate != previousDate;
36}
37
38// Custom Function: todayAsString
39// Arguments: none
40// Return type: String
41String todayAsString() {
42 final now = DateTime.now();
43 final y = now.year.toString();
44 final m = now.month.toString().padLeft(2, '0');
45 final d = now.day.toString().padLeft(2, '0');
46 return '$y-$m-$d';
47}
48
49// Custom Function: totalMinutesForDay
50// Arguments: entries (List<dynamic>)
51// Return type: int
52int totalMinutesForDay(List<dynamic> entries) {
53 int total = 0;
54 for (final e in entries) {
55 total += ((e['durationSeconds'] as int?) ?? 0);
56 }
57 return total ~/ 60;
58}

Common mistakes when creating a Time Tracking Tool in FlutterFlow

Why it's a problem: Storing timer state only in Page State — navigating away loses the running timer

How to avoid: Write isRunning and startTime to App State (global) as soon as the timer starts. On page load, check App State.isRunning and resume the timer if it is true.

Why it's a problem: Using a local DateTime.now() difference on the client to calculate elapsed time instead of storing startTime in Firestore

How to avoid: Store startTime as a Firestore Timestamp at the moment the user taps Start (a server-written field). Calculate duration as endTime minus startTime using both Timestamps, not client-side arithmetic.

Why it's a problem: Querying all time_entries for the user with no limit — loading years of data on every History page open

How to avoid: Add a where clause filtering date greater than or equal to 30 or 90 days ago. Implement Load More pagination using FlutterFlow's Infinite Scroll on the ListView.

Best practices

  • Store startTime as a Firestore server timestamp (FieldValue.serverTimestamp) to avoid clock drift across devices.
  • Save timer state to App State variables so the timer survives page navigation.
  • Debounce the periodic action cleanup — always cancel the Timer action when the Stop button is tapped to prevent memory leaks.
  • Index Firestore time_entries on (userId, date) to keep History queries fast as data grows.
  • Validate that a project is selected before allowing the timer to start — show a Snack Bar error if selectedProjectId is empty.
  • Display total hours worked today in the header using a simple Firestore aggregate query filtered to todayAsString().
  • Limit CSV exports to 1,000 rows by default and offer a date range picker for larger exports to keep Cloud Function execution times short.

Still stuck?

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

ChatGPT Prompt

I'm building a time tracking app in FlutterFlow using Firestore. I have a Page State variable called elapsedSeconds that increments every second via a periodic action. When the user navigates away, the timer resets. How should I persist the running timer state across pages using App State, and how do I resume it correctly on page load?

FlutterFlow Prompt

Create a Custom Function in FlutterFlow called formatElapsed that takes an integer argument elapsedSeconds and returns a String formatted as HH:MM:SS. Also create a function called todayAsString that returns today's date as a YYYY-MM-DD string with no arguments.

Frequently asked questions

Can I run the timer in the background on iOS and Android?

FlutterFlow does not support background tasks out of the box. The workaround is to save startTime to Firestore (or App State) when the timer starts. When the user returns to the app, calculate elapsed time as DateTime.now() minus startTime — this gives accurate duration even if the app was closed.

How do I prevent two timers from running at the same time?

Check isRunning in App State before starting a new timer. If it is already true, show a Snack Bar saying 'A timer is already running' and do not start a second one. Enforce this check in the Start button's conditional action.

How do I show total hours tracked per project in a summary view?

Query time_entries filtered by userId and projectId, then use a Custom Function to sum the durationSeconds field across all returned documents. Divide by 3600 and round to two decimal places for hours.

What happens if the user force-closes the app while a timer is running?

If you saved startTime to Firestore when the timer started, you can detect an orphaned entry on next app launch. Check App State.isRunning on the App Start action — if true and there is a startTime, resume the timer display from DateTime.now() minus startTime.

Can I track time offline and sync when the user reconnects?

Firestore has built-in offline persistence that queues writes when offline and syncs automatically on reconnect. Enable it in FlutterFlow's Firebase settings. Timer entries created offline will sync when connectivity returns.

How do I let users manually edit the start or end time of an entry?

Add a DateTime Picker widget to an edit dialog. Pre-fill it with the entry's startTime or endTime field. On save, update the Firestore document and recalculate durationSeconds as endTime minus startTime in a Custom Function.

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.