SonarFit Flutter Plugin
Official Flutter plugin for the SonarFit SDK - automatic rep counting from Apple Watch or AirPods Pro motion sensors.
Features
- Real-time Rep Detection: Motion-sensor analysis counts reps automatically
- Multiple Exercise Support: Squats & deadlifts on AirPods; squats, deadlifts, bench press, shoulder press & bicep curls on Apple Watch
- Device Flexibility: Works with Apple Watch or AirPods Pro
- Built-in UI: Beautiful, ready-to-use workout interface
- Progress Tracking: Automatic set/rep tracking with rest timers
- Permission Handling: Automatic motion permission requests
Platform Support
| Platform | Support |
|---|---|
| iOS | ✅ 17.0+ |
| Android | ❌ Not supported |
Installation
Add this to your pubspec.yaml:
dependencies:
sonarfit_flutter: ^2.4.1
Then run:
flutter pub get
iOS Configuration
- Update iOS deployment target in
ios/Podfile:
platform :ios, '17.0'
- Add required permissions to
ios/Runner/Info.plist:
<key>NSMotionUsageDescription</key>
<string>SonarFit needs motion sensor access to count your reps during workouts</string>
<key>NSHealthUpdateUsageDescription</key>
<string>SonarFit needs HealthKit access to save your workout data</string>
<key>NSHealthShareUsageDescription</key>
<string>SonarFit needs HealthKit access to read your workout history</string>
- Install CocoaPods dependencies:
cd ios && pod install && cd ..
Quick Start
1. Initialize the SDK
Get your API key from https://sonarfit.com/get-started
import 'package:sonarfit_flutter/sonarfit_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SonarFit SDK
await SonarFit.initialize('your-api-key-here');
runApp(MyApp());
}
2. Present a Workout
import 'package:flutter/material.dart';
import 'package:sonarfit_flutter/sonarfit_flutter.dart';
class WorkoutScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('SonarFit Workout')),
body: Center(
child: ElevatedButton(
onPressed: () async {
try {
final result = await SonarFit.presentWorkout(
WorkoutConfig(
workoutType: WorkoutType.squat,
sets: 3,
reps: 10,
deviceType: DeviceType.watch,
restTime: 60,
),
);
if (result.status == WorkoutStatus.completed) {
print('Workout completed!');
print('Reps: ${result.totalRepsCompleted}/${result.totalTargetReps}');
} else {
print('Workout stopped early');
print('Reps: ${result.totalRepsCompleted}/${result.totalTargetReps}');
}
} on SonarFitException catch (e) {
print('Error: ${e.message}');
}
},
child: Text('Start Workout'),
),
),
);
}
}
API Reference
SonarFit
Main entry point for the SDK.
Methods
initialize(String apiKey)
Initialize the SDK with your API key. Must be called before using any other methods.
await SonarFit.initialize('your-api-key');
Throws: SonarFitException if initialization fails.
presentWorkout(WorkoutConfig config)
Present SonarFit's built-in workout UI.
final result = await SonarFit.presentWorkout(
WorkoutConfig(
workoutType: WorkoutType.squat,
sets: 3,
reps: 10,
deviceType: DeviceType.watch,
restTime: 60,
countdownDuration: 3,
autoReLift: true,
),
);
Returns: WorkoutResult when workout completes (either fully or stopped early).
Throws: SonarFitException if workout fails to start, permission is denied, or user cancels before starting.
WorkoutConfig
Configuration for a workout session.
Properties
| Property | Type | Required | Description |
|---|---|---|---|
workoutType |
WorkoutType |
Yes | Type of exercise |
sets |
int |
Yes | Number of sets |
reps |
int |
Yes | Target reps per set |
deviceType |
DeviceType |
Yes | Motion tracking device (.watch or .airpods) |
restTime |
int |
No | Rest duration in seconds |
countdownDuration |
int |
No | Countdown before each set |
autoReLift |
bool |
No | Auto-detect reps |
Example
final config = WorkoutConfig(
workoutType: WorkoutType.deadlift,
sets: 5,
reps: 5,
deviceType: DeviceType.watch,
restTime: 90,
countdownDuration: 5,
autoReLift: true,
);
WorkoutType
Supported exercise types.
enum WorkoutType {
squat, // AirPods or Apple Watch
deadlift, // AirPods or Apple Watch
benchpress, // Apple Watch only
shoulderPress, // Apple Watch only
bicepCurl, // Apple Watch only
}
DeviceType
Motion tracking devices.
enum DeviceType {
watch, // Apple Watch
airpods, // AirPods Pro/Max with motion sensors
}
Note: DeviceType.none exists for internal SDK use only. Clients must explicitly select either .watch or .airpods.
WorkoutStatus
Workout completion status.
enum WorkoutStatus {
completed, // All target sets finished
stoppedEarly, // User ended before completion
}
WorkoutResult
Result of a completed workout.
Properties
| Property | Type | Description |
|---|---|---|
workoutType |
WorkoutType |
Exercise type |
deviceType |
DeviceType |
Device used for tracking |
startTime |
DateTime |
Workout start time |
endTime |
DateTime |
Workout end time |
totalDuration |
double |
Total duration in seconds |
status |
WorkoutStatus |
Workout completion status |
completionPercentage |
double |
Completion percentage (0.0-1.0) |
targetSets |
int |
Target number of sets |
targetRepsPerSet |
int |
Target reps per set |
totalRepsCompleted |
int |
Total reps completed |
totalTargetReps |
int |
Total target reps |
sets |
List<WorkoutSet> |
Per-set details |
Example
print('Completed ${result.totalRepsCompleted}/${result.totalTargetReps} reps');
print('${(result.completionPercentage * 100).toStringAsFixed(0)}% complete');
print('Status: ${result.status}');
for (var set in result.sets) {
print('Set ${set.setNumber}: ${set.repsCompleted} reps');
}
SonarFitException
Exception thrown by SDK operations.
Properties
| Property | Type | Description |
|---|---|---|
code |
String |
Error code |
message |
String |
Human-readable error message |
details |
dynamic |
Additional error details |
Common Error Codes
| Code | Description |
|---|---|
E_INIT_FAILED |
SDK initialization failed |
E_PERMISSION |
Motion permission denied |
E_INVALID_CONFIG |
Invalid workout configuration |
E_NO_ROOT_VC |
Cannot find root view controller |
E_CANCELLED |
Workout was cancelled by user |
E_NO_RESULT |
Workout completed but no result was returned |
Examples
Complete Workout Flow
import 'package:flutter/material.dart';
import 'package:sonarfit_flutter/sonarfit_flutter.dart';
class WorkoutExampleScreen extends StatefulWidget {
@override
_WorkoutExampleScreenState createState() => _WorkoutExampleScreenState();
}
class _WorkoutExampleScreenState extends State<WorkoutExampleScreen> {
WorkoutResult? _lastResult;
bool _isLoading = false;
Future<void> _startWorkout() async {
setState(() => _isLoading = true);
try {
final result = await SonarFit.presentWorkout(
WorkoutConfig(
workoutType: WorkoutType.squat,
sets: 3,
reps: 10,
deviceType: DeviceType.watch,
restTime: 60,
),
);
setState(() {
_lastResult = result;
_isLoading = false;
});
_showCompletionDialog(result);
} on SonarFitException catch (e) {
setState(() => _isLoading = false);
_showErrorDialog(e);
}
}
void _showCompletionDialog(WorkoutResult result) {
final title = result.status == WorkoutStatus.completed
? 'Workout Complete!'
: 'Workout Stopped';
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Reps: ${result.totalRepsCompleted}/${result.totalTargetReps}'),
Text('Completion: ${(result.completionPercentage * 100).toStringAsFixed(0)}%'),
SizedBox(height: 16),
Text('Sets:', style: TextStyle(fontWeight: FontWeight.bold)),
...result.sets.map((set) =>
Text('Set ${set.setNumber}: ${set.repsCompleted} reps')),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
void _showErrorDialog(SonarFitException error) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(error.message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('SonarFit Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isLoading)
CircularProgressIndicator()
else
ElevatedButton(
onPressed: _startWorkout,
child: Text('Start Workout'),
),
if (_lastResult != null) ...[
SizedBox(height: 32),
Text(
'Last Workout',
style: Theme.of(context).textTheme.headlineSmall,
),
SizedBox(height: 8),
Text('${_lastResult!.totalRepsCompleted} reps completed'),
Text('${(_lastResult!.completionPercentage * 100).toStringAsFixed(0)}% complete'),
],
],
),
),
);
}
}
Multiple Workout Types
class WorkoutTypeSelector extends StatelessWidget {
final List<WorkoutType> workouts = [
WorkoutType.squat,
WorkoutType.deadlift,
WorkoutType.benchpress,
WorkoutType.shoulderPress,
WorkoutType.bicepCurl,
];
Future<void> _startWorkout(BuildContext context, WorkoutType type) async {
try {
final result = await SonarFit.presentWorkout(
WorkoutConfig(
workoutType: type,
sets: 3,
reps: 10,
deviceType: DeviceType.watch,
),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${result.totalRepsCompleted} reps completed (${result.status.name})')),
);
} on SonarFitException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${e.message}')),
);
}
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: workouts.length,
itemBuilder: (context, index) {
final workout = workouts[index];
return ListTile(
title: Text(_getWorkoutName(workout)),
trailing: Icon(Icons.arrow_forward),
onTap: () => _startWorkout(context, workout),
);
},
);
}
String _getWorkoutName(WorkoutType type) {
switch (type) {
case WorkoutType.squat:
return 'Squat';
case WorkoutType.deadlift:
return 'Deadlift';
case WorkoutType.benchpress:
return 'Bench Press';
}
}
}
Pricing
SonarFit uses MAU-based pricing (Monthly Active Users). A user counts as active when they complete at least one workout in a calendar month.
| Tier | Price | Included MAUs | Overage |
|---|---|---|---|
| Developer | Free | 500 (hard cap) | N/A |
| Startup | £129/mo | 2,000 | £0.08/MAU |
| Growth | £599/mo | 10,000 | £0.07/MAU |
| Enterprise | Custom | Negotiated | Custom |
Get started at https://sonarfit.com/pricing
Requirements
- Flutter: 3.3.0 or higher
- Dart: 3.0.0 or higher
- iOS: 17.0 or higher
- Xcode: 16.0 or higher
- CocoaPods: 1.11.0 or higher
Troubleshooting
Build Errors
Error: "Platform version is lower than 17.0"
Solution: Update ios/Podfile:
platform :ios, '17.0'
Error: "No such module 'SonarFitKit'"
Solution: Run:
cd ios && pod install && cd ..
flutter clean
flutter pub get
Runtime Errors
Error: "Motion permission denied"
Solution: Add NSMotionUsageDescription to Info.plist and ensure user grants permission when prompted.
Error: "SDK not initialized"
Solution: Call SonarFit.initialize() before using any other methods.
Support
- Documentation: https://sonarfit.com/docs
- Email: support@sonarfit.com
- GitHub Issues: https://github.com/sonarfit/sonarfit-flutter/issues
License
MIT License - see LICENSE file for details
About SonarFit
SonarFit provides automatic rep counting for fitness apps. Our SDK handles real-time rep counting and workout progression so you can focus on building great user experiences.
Learn more at https://sonarfit.com
Native Watch app + Flutter phone app (headless watch detection)
If your Watch app is native Swift and counts reps with SonarFit inside its own UI (headless detection, see the SonarFit iOS integration guide), the Flutter phone app needs only:
await SonarFit.initialize('sk_live_...'); // licensing and usage metering of the watch's workouts
Optionally mirror the sets live in Flutter:
SonarFit.headlessEvents.listen((e) {
switch (e.type) {
case 'setStarted': /* e.exercise, e.targetReps, e.setIndex */ break;
case 'rep': /* e.count, cumulative */ break;
case 'setEnded': /* e.reps, e.duration, e.exercise */ break;
case 'workoutEnded': /* e.setsCounted */ break;
}
});
The Watch target links the SonarFit Swift package directly (SonarFitKit, watchOS 10+) and uses
SonarFit.startHeadlessWorkout() / startRepDetection / endHeadlessWorkout(save:), or its
own HKWorkoutSession with configureWatchSession(.hostProvided(...)). Requires SonarFit 2.6.0.