Home Pricing Explore MCP Docs Create a project

Home/Blog/Guides

Build an Interval Workout Timer with FlutterGo, Then Check Its Timer Code

A hands-on FlutterGo tutorial: one prompt to a working Flutter interval timer, four bugs found by testing, one follow-up fix, and a timer code review.

A timer app looks simple until you write one. It has to count down accurately, move through phases in the right order, survive Pause and Skip, and record what actually happened. That makes it a good test for an AI app builder.

In this tutorial we build Pulse, an interval workout timer, from one prompt in FlutterGo. Then we test it like a user, fix the four bugs we found with one follow-up prompt, and read the timer code to find what testing can't show.

Everything below happened on October 1, 2026, in the FlutterGo studio. The screenshots and the demo video come from that session.

Key takeaways

  • One prompt produced a working Flutter timer with presets, a phase-colored countdown ring, Pause/Skip/Reset, an editor and a history screen
  • Testing found four real bugs, including a Save button that silently failed. One follow-up prompt fixed all four in under two minutes
  • The timer counts with Timer.periodic, which is fine for a demo but drifts when the app is paused or backgrounded. Reading the code is how you find that

Try Pulse yourself: open the live app. This is FlutterGo's public share link for the fixed build from this tutorial. It runs in the browser, so it works on a phone or a laptop and needs no install or sign-in.

Time: about 30 minutes, most of it waiting on the first build. You need: a FlutterGo account. No local Flutter install is required.

Pulse in 21 seconds: the prompt, the workout list, a session moving through Warm-up, Work, Rest and Done, and the workout editor. Every phone screen is a real screenshot from the build.

Step 1: Describe the timer in one prompt

Open FlutterGo and type the app into the box on the studio home screen. For a timer, the important part is the behavior, so spell out the phases and the controls. Here's the exact prompt we used:

An interval workout timer called Pulse. Screens: a Workouts list with a few
presets (Tabata 20/10 x8, HIIT 40/20 x10, EMOM 60s x12); a Workout editor to set
name, work seconds, rest seconds, rounds and a warm-up; and a full-screen Timer
screen with a big countdown ring, the current phase (Warm-up, Work, Rest, Done),
round X of Y, and Start/Pause, Skip and Reset buttons. The ring color changes by
phase and the last 3 seconds of each phase show a bold countdown. Dark,
high-contrast sporty design.
The FlutterGo studio home screen with the Pulse prompt typed into the description box and the Start building button.
Step 1: three screens, four phases, three controls, one paragraph.

The prompt names the presets with real numbers, which gives the agent concrete data to build and gives you something to check later. It also lists every phase and every control. Leave one of them out and you're relying on the agent to guess it.

Click Start building. When FlutterGo asks where data should live, choose Skip for now. Pulse doesn't need a backend, so it keeps everything in memory.

Step 2: Let it build, then run the preview

The workspace opens with the chat on the left and an iPhone 16 Pro preview on the right. Our first build finished in 12 minutes 46 seconds. Then FlutterGo ran flutter analyze on the project before building the preview. Once that passed, Run app compiled the web preview.

The FlutterGo workspace after the build: the chat lists what was built and shows Done 12m 46s, and the iPhone preview shows Pulse's Workouts screen.
Step 2: the build summary and the running preview.

FlutterGo's summary listed five areas: Workouts, Editor, Timer, History and Profile. We only asked for the first three. It also added a fourth preset, Sprint Ladder 30/15 ×6, and filled History and Profile with sample data for a made-up user called Maya Chen. That's helpful for a demo, but you'll want to remove it before shipping.

Step 3: Test the timer like a user

Open a preset and run it. Then poke at every control you asked for, not just the happy path.

Two Pulse screens side by side: the Workouts list with a Quick start card for Tabata 20/10 ×8, and the timer ready to start in the Warm-up phase at 30 seconds.
The Workouts list and the timer, ready at the start of warm-up.

Here's what worked on the first build:

  1. Phases in order. Tabata 20/10 ×8 ran a 30-second warm-up (amber), then 20 seconds of Work (red) and 10 seconds of Rest (cyan), and repeated.
  2. Round counter. The header moved from Round 1 of 8 to Round 2 of 8 at the start of each new work phase.
  3. Pause. Pause froze the countdown and switched the button label back to Start.
  4. Done state. After the last rest, the ring turned lime and showed NAILED IT and Session complete.
Two timer screens: the Work phase at 14 seconds with a red ring and the Rest phase at 7 seconds with a cyan ring and the caption Breathe, next work soon.
Each phase gets its own ring color.

Then we tried the edges, and four things broke.

Bug 1: Save on a preset did nothing. We opened the Tabata editor, raised Work from 20 to 25 and tapped Save. The screen didn't close. After we went back, the card still said 20/10. The browser console showed why:

Unsupported operation: '[]=': Cannot modify a constant list
The Edit workout screen with Work set to 25, next to the Workouts list where the Tabata card still shows 20/10 × 8 after Save.
Bug 1: we saved 25 seconds of work, and the list still shows 20.

Bug 2: Skipping logged a full workout. We tapped Skip through every phase without doing a single second of work. Pulse said NAILED IT, and History added "Today · 8 rounds · 72 kcal".

The Done screen reading NAILED IT after skipping every phase, next to History showing a new Tabata session logged today with 8 rounds and 72 kcal.
Bug 2: zero work, eight rounds logged.

Bug 3: The Quick start card had the wrong number. It said "4 min work" for Tabata 20/10 ×8. Eight 20-second rounds is 2:40 of work. Four minutes is the work plus the rest, before warm-up.

Bug 4: The chart overflowed its card. In History's Weekly work volume, the tallest bar pushed its day label below the bottom of the card (you can see the stray "T" in the screenshot above).

None of these show up on the first screen. You only find them by using the app and trying things a real user would do.

Step 4: Fix all four with one follow-up prompt

Put all your fixes in one message. Describe each bug by what you saw. If you've seen the cause, like the console error, include it.

Four fixes: 1) Editing a preset and tapping Save does nothing. The console says
"Cannot modify a constant list" because presets() returns a const list. Copy it
into a growable list in PulseStore. 2) Skipping through every phase logs a full
session in History even though no work was done. Only log rounds whose work phase
actually ran, and don't log a session if none did. 3) The Quick start banner says
"4 min work" for Tabata 20/10 x8, which is 2:40 of work. Compute the line from
the workout, and always start the Tabata preset, not whatever is first in the
list. 4) On History, the tallest bar in Weekly work volume pushes its day label
below the card. Keep all bars and labels inside the card.

FlutterGo answered in 1 minute 48 seconds, said all four were fixed and hot-restarted the preview. Its file list also showed seven new asset files that its summary didn't explain: four navigation icons (nav_home.svg, nav_play.svg, nav_learn.svg, nav_pals.svg) and three Lottie files (empty.json, success.json, loader.json). Pulse doesn't have screens with those names. Check files like these before you commit, and delete the ones nothing uses.

Then test again. A summary saying something is fixed doesn't prove it. We repeated every failing step on the new build:

  • Editing Tabata to 25 seconds and tapping Save closed the editor. The card now showed 25/10 × 8 and about 6 minutes.
  • The Quick start line, now calculated from the workout, read 3:20 work (25 × 8).
  • Skipping through every phase still ended on the Done screen, but History logged nothing new.
  • All the chart's bars and labels stayed inside the card.
After the fix: the Workouts list with the Tabata card showing 25/10 × 8 and a Quick start line of 3:20 work, and History with no session logged for the skipped run and the chart labels inside the card.
After the fix: the saved edit sticks, and skipping no longer counts as a workout.

Step 5: Review the timer code

A preview can show you behavior. It can't show you how the timer is built. Open the files from the Show code panel, or use Download code or GitHub to get the whole project in your editor (export docs). Here's what we found in Pulse.

Structure. Code is split by feature: lib/features/ holds workouts, editor, timer, history, profile and a shell for the bottom tabs. Models and the store live under lib/core/. Routing is go_router with a StatefulShellRoute.indexedStack for the three tabs, plus top-level /editor/:id and /timer/:id routes.

State. All data sits in one PulseStore extends ChangeNotifier singleton. Screens subscribe with addListener in initState and call setState. That's plain, readable Flutter, and it's enough for an app this size. Our state management guide covers when to move to something bigger.

How the countdown works. The timer screen ticks with Timer.periodic(const Duration(seconds: 1), ...) and subtracts 1 from an integer each tick:

void _tick() {
  if (_secondsLeft > 1) {
    setState(() => _secondsLeft -= 1);
    if (_secondsLeft <= 3) {
      HapticFeedback.heavyImpact();
    }
    return;
  }
  HapticFeedback.mediumImpact();
  _advancePhase(workCompleted: _phase == TimerPhase.work);
}

This works while the app is in the foreground and nothing gets in the way, but it counts ticks, not time. Two problems follow:

  • Pause loses part of a second. Pausing cancels the timer. Resuming starts a fresh one-second wait, so whatever was left of the current second is lost, or added, depending on how you see it.
  • The countdown falls behind when the app can't run its timers. We saw this in the web preview. With the preview tab in the background, Chrome throttles timers to roughly once a second, and to once a minute after a page has been hidden for more than five minutes (Chrome's timer throttling rules). The countdown fell behind real time and then jumped ahead. On a phone, a backgrounded or locked app has the same problem.

The usual fix is to store when the current phase ends, then work out the seconds left from the clock each time you redraw. A Timer then only triggers repaints and no longer keeps the time. For a mobile build, also listen for app lifecycle changes so the phase moves on correctly when the user comes back. That's one more good follow-up prompt.

The bold countdown is there. We read this in the code because our screenshots never caught those three seconds. While the timer is running and three or fewer seconds are left, a boldEnd flag grows the number from 84 to 108 points, colors it to match the phase and shows PUSH under the ring. One catch: it's tied to _running, so pausing in the last three seconds switches the bold styling off.

Smaller things worth a follow-up:

  • Preset names include their numbers. After we changed Tabata's work time to 25 seconds, the card still read Tabata 20/10 ×8 above a 25/10 × 8 chip. Build the name from the values or stop putting numbers in it.
  • Done still says "NAILED IT" after you skip everything. The fix stopped the bogus History entry, but the celebration screen hasn't caught up.
  • The analyzer flagged an unused variable (rest in interval_workout.dart, in the EMOM branch of restTotalSeconds). It's harmless, but a sign the logic there was rewritten partway through.
  • Dependencies. pubspec.yaml pins go_router: ^14.8.1, and the current release on pub.dev is 18.0.2. Screens import package:flutter/material.dart. That still works, but Flutter is moving Material into the separate material_ui package, so plan the Material and Cupertino migration for this code too.
  • Tests. The project ships one widget test that checks the app builds and shows "Workouts". The phase logic in _advancePhase is the code that most needs unit tests: warm-up into round 1, rest into the next round, the last rest into Done, EMOM with no rest, and Skip. Ask for those next.

A checklist for any timer you generate

  1. Name every phase and control in the prompt, with real numbers for the presets.
  2. Run a full session, then test Pause and Skip, including skipping all the way to the end.
  3. Check the numbers the app prints against arithmetic you can do yourself.
  4. Edit something and save it. Persistence bugs hide behind screens that look finished.
  5. Open the browser console while you test the web preview. It caught our Save bug instantly.
  6. Re-test after every fix, and look at the file list for things the summary didn't mention.
  7. Read how the timer keeps time. If it counts ticks instead of comparing against the clock, plan to change it before anyone relies on it in a workout.

Where to go next

Pulse is a solid first draft. It builds, every requested screen works, the four bugs we found were fixed in one pass, and the code is organized the way a Flutter developer would expect. Before anyone uses it in a real workout, it needs a clock-based countdown, lifecycle handling and a few unit tests around phase changes.

You can try the finished Pulse app here. To build your own, open the FlutterGo studio and start from the prompt above. Then go one step further than we did: connect GitHub, open the project locally, and ask for the clock-based timer.


Built and tested in FlutterGo on October 1, 2026. The app ran in FlutterGo's web preview in an iPhone 16 frame. We didn't install it on a physical device, and we didn't run flutter test outside FlutterGo. The demo video was assembled from real screenshots of this build. The phone screens weren't redrawn or edited.

  • Tutorial
  • FlutterGo
  • Flutter
  • Timer
  • go_router

Build the app you just read about. Describe it in plain English — FlutterGo writes real Flutter code and gets it ready for both stores.

3 posts