
A production-grade, accurate step counter for Flutter on Android. Uses the native TYPE_STEP_COUNTER sensor via a foreground service for reliable tracking across foreground, background, and terminated app states.
- ๐ฏ Hardware Accurate โ Uses Android's
TYPE_STEP_COUNTER sensor (cumulative, boot-relative)
- ๐พ Persistent Storage โ SQLite database with reactive streams and background isolate support
- ๐ฑ All App States โ Foreground, background, AND terminated state recovery
- ๐ Smart Merge โ
SmartMergeHelper.mergeStepCounts() for combining sensor + Health Connect + server sources
- ๐ Battery Efficient โ Event-driven architecture with notification throttling
- โฑ๏ธ Warmup Validation โ Filters out shakes and false positives with sliding window validation
- ๐งต Low-End Device Support โ Optional background isolate for smooth UI on budget devices
- ๐ External Import โ Import steps from Google Fit, Apple Health, etc.
- ๐ Midnight Day Reset โ Automatic day boundary handling with alarm-based midnight reset
| Platform |
Status |
Note |
| Android |
โ
Full support (API 24+) |
Native TYPE_STEP_COUNTER + foreground service |
| iOS |
โ Not supported |
|
dependencies:
accurate_step_counter: ^2.0.0
In android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_HEALTH"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
import 'package:accurate_step_counter/accurate_step_counter.dart';
final stepCounter = AccurateStepCounter();
// Initialize database + start detection + start logging
await stepCounter.initializeLogging(useBackgroundIsolate: true);
await stepCounter.start(config: StepDetectorConfig.walking());
await stepCounter.startLogging(config: StepRecordConfig.aggregated());
// Watch today's steps (emits immediately with stored value)
stepCounter.watchAggregatedStepCounter().listen((steps) {
print('Steps today: $steps');
});
import 'package:flutter/material.dart';
import 'package:accurate_step_counter/accurate_step_counter.dart';
import 'package:permission_handler/permission_handler.dart';
class StepCounterPage extends StatefulWidget {
@override
State<StepCounterPage> createState() => _StepCounterPageState();
}
class _StepCounterPageState extends State<StepCounterPage>
with WidgetsBindingObserver {
final _stepCounter = AccurateStepCounter();
int _steps = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_init();
}
Future<void> _init() async {
await Permission.activityRecognition.request();
await _stepCounter.initializeLogging(useBackgroundIsolate: true);
await _stepCounter.start(config: StepDetectorConfig.walking());
await _stepCounter.startLogging(config: StepRecordConfig.aggregated());
_stepCounter.watchAggregatedStepCounter().listen((steps) {
setState(() => _steps = steps);
});
_stepCounter.onTerminatedStepsDetected = (steps, from, to) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Synced $steps missed steps!')),
);
};
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
_stepCounter.setAppState(state);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_stepCounter.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('$_steps steps', style: TextStyle(fontSize: 48)),
),
);
}
}
Combine multiple step sources for maximum reliability โ the pattern used by production apps:
import 'package:accurate_step_counter/accurate_step_counter.dart';
final merged = SmartMergeHelper.mergeStepCounts(
sensorSteps: await stepCounter.currentStepCount,
healthConnectSteps: hcSteps ?? 0,
serverSteps: serverRecoveredSteps,
currentDisplayed: displayedCount,
);
// Always returns the highest reliable value โ never goes backwards
Android TYPE_STEP_COUNTER sensor
โ
StepCounterService.kt (Foreground service with notification)
โ LocalBroadcastManager
AccurateStepCounterPlugin.kt (MethodChannel + EventChannel bridge)
โ EventChannel stream
step_counter_platform.dart (Dart platform interface)
โ
AccurateStepCounterImpl (Detection + SQLite logging + aggregation)
โ
Your App (watchAggregatedStepCounter / SmartMergeHelper)
| App State |
Behavior |
| ๐ข Foreground |
Real-time step events via EventChannel |
| ๐ก Background |
Foreground service keeps counting |
| ๐ด Terminated |
TYPE_STEP_COUNTER sync on restart + midnight alarm reset |
| Method |
Description |
start() |
Start step detection |
stop() |
Stop step detection |
isNativeStepServiceRunning() |
Check if native service is alive |
isUsingNativeStepService |
Whether native service mode is active |
currentStepCount |
Current steps since start() |
| Method |
Description |
initializeLogging() |
Initialize SQLite database |
startLogging() |
Start recording steps to database |
watchAggregatedStepCounter() |
Real-time stream (stored + live) |
aggregatedStepCount |
Sync getter for current total |
getTodaySteps() |
Today's total from database |
getYesterdaySteps() |
Yesterday's total from database |
getStepsInRange() |
Steps for custom date range |
writeStepsToAggregated() |
Import external steps |
| Method |
Description |
SmartMergeHelper.mergeStepCounts() |
Merge sensor + HC + server (max of all) |
SmartMergeHelper.mergeSensorAndHealth() |
Simplified merge (sensor + HC only) |
| Method |
Description |
StepLogsViewer |
Widget for viewing step logs with filters |
getStepLogs() |
Get all step log entries |
getStepStats() |
Get statistics (totals by source, averages) |
// Presets
await stepCounter.start(config: StepDetectorConfig.walking());
await stepCounter.start(config: StepDetectorConfig.running());
await stepCounter.start(config: StepDetectorConfig.sensitive());
await stepCounter.start(config: StepDetectorConfig.conservative());
// Custom
await stepCounter.start(config: StepDetectorConfig(
threshold: 1.2,
filterAlpha: 0.85,
minTimeBetweenStepsMs: 250,
enableOsLevelSync: true,
useForegroundServiceOnOldDevices: true,
foregroundServiceMaxApiLevel: 29,
));
// Logging presets
await stepCounter.startLogging(config: StepRecordConfig.aggregated());
await stepCounter.startLogging(config: StepRecordConfig.lowEndDevice());
- Idempotency keys โ Deterministic keys prevent duplicate records
- Single-writer queue โ Serialized database writes prevent race conditions
- Mutex locks โ Concurrent
writeStepsToAggregated() calls are serialized
- Warmup validation โ Sliding window filters shakes and non-walking motion
- Terminated sync โ Deterministic gap identity prevents double-counting
- Midnight reset โ AlarmManager-based day boundary handling
- Cold start recovery โ Database auto-reopens after Android kills the app
// Background isolate moves all DB work off the main thread
await stepCounter.initializeLogging(useBackgroundIsolate: true);
await stepCounter.startLogging(config: StepRecordConfig.lowEndDevice());
MIT License โ see LICENSE