fitness_workout
A Flutter package for running, tracking and finishing workouts.
It ships:
WorkoutRunner— aChangeNotifier-based controller with timers, set/exercise indices, plan mutation, persistence and aStream<WorkoutResult>finished hook.CardioRunner— interval / lap based companion controller for running, cycling, rowing, HIIT, jump rope, etc. Same lifecycle shape, same storage interface, a separate slot so it coexists with the strength runner.- Pluggable
RunnerStoragewithSharedPreferencesand in-memory implementations. Roll your own to point at SQLite, Hive, Supabase, … - Drop-in widgets in a dark-first fitness style for both sides:
RunnerPanel,QuickRunner,ResultsView,RunnerStatusChip/Banner/BottomBar, fullRunnerScreen, and their cardio peers (CardioRunnerPanel,CardioQuickRunner,CardioResultsView,CardioRunnerStatusChip/Banner/BottomBar,CardioRunnerScreen). WorkoutRunnerThemewith design tokens you can override to re-style every bundled widget.
Requires Flutter ≥ 3.16 / Dart ≥ 3.7.
Install
dependencies:
fitness_workout: ^1.0.0
import 'package:fitness_workout/fitness_workout.dart';
Screenshots
![]() Quick runner |
![]() Runner panel |
![]() Set input |
![]() Rest overlay |
![]() Results |
![]() Cardio runner |
Quick start
final runner = WorkoutRunner();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await runner.tryAutoResume(); // optional — resumes a previous workout
runApp(MaterialApp(home: Home(runner: runner)));
}
class Home extends StatelessWidget {
final WorkoutRunner runner;
const Home({super.key, required this.runner});
@override
Widget build(BuildContext context) {
final plan = WorkoutPlan(
id: 'fullbody',
name: 'Full body',
exercises: DefaultExercises.all.take(5).toList(),
);
return RunnerScope(
runner: runner,
child: Scaffold(
appBar: AppBar(
title: const Text('Workouts'),
actions: const [RunnerStatusAppBarAction()],
),
body: QuickRunner(
plans: [plan],
onOpen: (p) => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => RunnerScreen(plan: p, runner: runner),
),
),
),
bottomNavigationBar: const RunnerStatusBottomBar(),
),
);
}
}
You can build plans fluently instead of nesting model constructors:
final plan = WorkoutPlanBuilder('Push Day')
.exercise('Bench Press')
.set(reps: 8, weight: 80, rest: const Duration(seconds: 90))
.set(reps: 8, weight: 80)
.exercise('Shoulder Press')
.set(reps: 10)
.build();
final validation = plan.validate();
if (!validation.isValid) {
for (final issue in validation.errors) {
debugPrint(issue.message);
}
}
Listen for finished workouts anywhere:
runner.finished.listen((result) {
// ship to backend, log to analytics, …
print('Done in ${result.duration}, '
'${result.totalSets} sets, '
'${result.totalVolume} kg total volume.');
});
Or attach lightweight lifecycle callbacks:
runner
..onSetCompleted = (set) => debugPrint('Set ${set.setIndex + 1} done')
..onExerciseChanged = (index) => debugPrint('Showing exercise $index')
..onRestStarted = (rest) => debugPrint('Rest: ${rest.inSeconds}s');
Cardio runner
CardioRunner is the interval-based companion to WorkoutRunner — for
plans built out of laps (CardioInterval) rather than (set × reps × weight) tuples.
final cardio = CardioRunner();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await cardio.tryAutoResume();
runApp(MaterialApp(home: CardioHome(runner: cardio)));
}
class CardioHome extends StatelessWidget {
final CardioRunner runner;
const CardioHome({super.key, required this.runner});
@override
Widget build(BuildContext context) {
return CardioRunnerScope(
runner: runner,
child: Scaffold(
appBar: AppBar(
title: const Text('Cardio'),
actions: const [CardioRunnerStatusChip(), SizedBox(width: 8)],
),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CardioRunnerScreen(
plan: DefaultCardioPlans.tabata,
runner: runner,
),
),
),
child: const Text('Start Tabata'),
),
),
),
);
}
}
CardioRunner defaults to slot 'cardio', while WorkoutRunner defaults
to slot 'default'. The two can share the same RunnerStorage without
collision, so you can nest both scopes near the root of your app and run
strength + cardio sessions side-by-side:
RunnerScope(
runner: strengthRunner,
child: CardioRunnerScope(
runner: cardioRunner,
child: const MaterialApp(home: Home()),
),
)
Cardio exposes the same kind of lightweight lifecycle hooks:
cardio
..onIntervalCompleted = (lap) => debugPrint('Lap ${lap.intervalIndex + 1}')
..onPaused = () => debugPrint('Cardio paused')
..onResumed = () => debugPrint('Cardio resumed');
See doc/ARCHITECTURE.md
for the coexistence model.
What's in the box
| Widget | Purpose |
|---|---|
RunnerPanel |
Plan header, exercise carousel, rest strip, finish button. |
RunnerScreen |
Stand-alone screen wrapping RunnerPanel + AppBar + result transition. |
QuickRunner |
"Start workout" picker that swaps to "Continue" when one is running. |
ResultsView |
Hero summary with stats and per-exercise breakdown. |
RunnerStatusChip |
Tiny "23:45" pill — safe to drop into any AppBar. |
RunnerStatusBanner |
Inline banner under your AppBar / hero block. |
RunnerStatusBottomBar |
Bottom-attached "continue workout" CTA. |
SetRow |
Individual set row with start / running / done states + input sheet. |
CardioRunnerPanel |
Hero timer + interval card + lap timeline + control bar. |
CardioRunnerScreen |
Stand-alone cardio screen + result transition. |
CardioQuickRunner |
"Start cardio" picker that swaps to "Cardio running" when active. |
CardioResultsView |
Hero summary with laps, work time, distance, average pace. |
CardioRunnerStatusChip |
Tiny accent pill with elapsed cardio time. |
CardioRunnerStatusBanner |
Inline banner highlighting the running cardio session. |
CardioRunnerStatusBottomBar |
Bottom-attached "continue cardio" CTA. |
Strength widgets read from the nearest RunnerScope; cardio
widgets read from the nearest CardioRunnerScope.
Theming
WorkoutRunnerTheme(
data: WorkoutRunnerThemeData.dark().copyWith(
accent: const Color(0xFFFF7E2A), // switch to orange
),
child: const RunnerScreen(...),
);
See doc/THEMING.md for the full token list.
Custom storage
class SupabaseRunnerStorage implements RunnerStorage {
// implement read/save/clear for state + plan
}
final runner = WorkoutRunner(storage: SupabaseRunnerStorage());
See doc/STORAGE.md.
Screenshots
The example app under example/ demonstrates every widget. Run it with
cd example && flutter run.
Documentation
| File | Content |
|---|---|
doc/ARCHITECTURE.md |
Module map, data flow, lifecycle |
doc/API.md |
Public API reference |
doc/WIDGETS.md |
Widget gallery with code snippets |
doc/STORAGE.md |
Implementing your own storage |
doc/THEMING.md |
Design tokens and overrides |
doc/MIGRATION.md |
Upgrading from 0.x to 1.0 |
License
MIT — see LICENSE.
Libraries
- fitness_workout
- Public API of the
fitness_workoutpackage.





