Accurate Step Counter
A simple, accurate step counter for Flutter. Works in foreground, background, and terminated states. Health Connect-like API with persistent storage.
โจ Features
- ๐ฏ Accurate - Uses sensors_plus accelerometer with peak detection algorithm
- ๐พ Persistent - Steps saved to local SQLite database
- ๐ฑ All States - Foreground, background, AND terminated
- ๐ Simple API - One-line setup, no complexity
- ๐ Battery Efficient - Event-driven, not polling
- โฑ๏ธ Inactivity Timeout - Auto-reset sessions after idle periods
- ๐ External Import - Import steps from Google Fit, Apple Health, etc.
- ๐งช Well Tested - 671 automated tests covering all scenarios
๐ก๏ธ Why it's Reliable (The ANR Fix)
Previous step counters (and earlier versions of this one) could freeze phones by asking "What time is it locally?" too often (50 times a second!). This forces the phone to read timezone files constantly, causing "Application Not Responding" (ANR) crashes on Android 12.
This package (v1.8.10+) uses UTC time for all high-speed processing. v1.8.11+ also handles cold start scenarios where Android kills the app. It only converts to "Local Time" when showing steps to the user. This means:
- Zero lag: The 50Hz sensor loop never blocks the main thread.
- Zero crashes: No timezone file lockups.
- 100% Accuracy: "Today" is still safely calculated based on the user's local midnight.
๐๏ธ Code Structure (Simple View)
SensorsStepDetector(The Eyes): Watches the accelerometer ~50 times a second to find generic movement.AccurateStepCounter(The Brain): Filters that movement. It ignores shakes and only counts real walking.StepRecordStore(The Memory): Saves every valid step to a local SQLite database so data is never lost.StepCounterForegroundService(The Night Watchman): Keeps "The Eyes" open even when the app is closed (on older Androids).
๐ฑ Platform Support
| Platform | Status | Note |
|---|---|---|
| Android | โ Full support (API 19+) | Includes critical ANR fix for Android 12 (v1.8.10+) |
| iOS | โ Not supported |
๐ Quick Start
1. Install
dependencies:
accurate_step_counter: ^1.9.1
2. Add Permissions
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"/>
3. Use It!
import 'package:accurate_step_counter/accurate_step_counter.dart';
final stepCounter = AccurateStepCounter();
// ๐ One-line setup!
await stepCounter.initSteps();
// Get today's steps
final todaySteps = await stepCounter.getTodayStepCount();
// Watch real-time updates
stepCounter.watchTodaySteps().listen((steps) {
print('Steps today: $steps');
});
// Get yesterday's steps
final yesterdaySteps = await stepCounter.getYesterdayStepCount();
// Custom date range
final weekSteps = await stepCounter.getStepCount(
start: DateTime.now().subtract(Duration(days: 7)),
end: DateTime.now(),
);
๐ Complete Example
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 {
// Request permissions
await Permission.activityRecognition.request();
// Initialize step counter
await _stepCounter.initSteps();
// Watch today's steps (emits immediately with stored value!)
_stepCounter.watchTodaySteps().listen((steps) {
setState(() => _steps = steps);
});
// Handle terminated state sync
_stepCounter.onTerminatedStepsDetected = (steps, from, to) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Synced $steps missed steps!')),
);
};
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
_stepCounter.setAppState(state); // Important for source tracking!
}
@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)),
),
);
}
}
๐ง API Reference
Core Methods
| Method | Description |
|---|---|
initSteps() |
One-line setup (DB + detector + logging) |
getTodayStepCount() |
Get today's total steps |
getYesterdayStepCount() |
Get yesterday's total steps |
getStepCount(start, end) |
Get steps for date range |
watchTodaySteps() |
Real-time stream of today's steps |
setAppState(state) |
Track foreground/background (call in didChangeAppLifecycleState) |
dispose() |
Clean up resources |
Reading Logs
// Get all step logs with details
final logs = await stepCounter.getStepLogs();
for (final log in logs) {
print('${log.stepCount} steps');
print('From: ${log.fromTime} To: ${log.toTime}');
print('Source: ${log.source}'); // foreground, background, terminated, external
}
// Filter by date or source
final todayLogs = await stepCounter.getStepLogs(from: startOfToday);
final bgLogs = await stepCounter.getStepLogs(source: StepRecordSource.background);
final externalLogs = await stepCounter.getStepLogs(source: StepRecordSource.external);
// Get stats
final stats = await stepCounter.getStepStats();
// {totalSteps, foregroundSteps, backgroundSteps, terminatedSteps, ...}
Importing External Steps (NEW in v1.6.0)
// Import steps from Google Fit, Apple Health, wearables, etc.
await stepCounter.writeStepsToAggregated(
stepCount: 500,
fromTime: DateTime.now().subtract(Duration(hours: 2)),
toTime: DateTime.now(),
source: StepRecordSource.external, // Mark as external import
);
// All listeners automatically notified!
// Query external steps
final externalSteps = await stepCounter.getStepsBySource(
StepRecordSource.external,
);
Data Management
// Clear all logs
await stepCounter.clearStepLogs();
// Delete old logs
await stepCounter.deleteStepLogsBefore(
DateTime.now().subtract(Duration(days: 30)),
);
Debug Logs Viewer (NEW in v1.8.2)
Display step logs visually with filtering and export:
import 'package:accurate_step_counter/accurate_step_counter.dart';
// Add to your widget tree
StepLogsViewer(
stepCounter: _stepCounter,
maxHeight: 300,
showFilters: true, // Filter by source (FG/BG/Term/Ext)
showExportButton: true, // Copy logs to clipboard
showDatePicker: true, // Date range filtering
)
Features:
- ๐ Filter by source (foreground/background/terminated/external)
- ๐ Date range picker
- ๐ Export to clipboard
- โก Real-time updates
- ๐จ Color-coded entries by source
๐ฑ How It Works (Hybrid Architecture v1.8.x)
| App State | Android โค10 (API โค29) | Android 11+ (API 30+) |
|---|---|---|
| ๐ข Foreground | sensors_plus accelerometer (realtime) | Native detector (realtime) |
| ๐ก Background | sensors_plus accelerometer (realtime) | Native detector (realtime) |
| ๐ด Terminated | Foreground service with sensors_plus | TYPE_STEP_COUNTER sync on restart |
Key Benefits (v1.8.0):
- โ More Reliable: sensors_plus provides consistent accelerometer access across devices
- โ Better UX: No persistent notification when app is running (Android 11+)
- โ Better battery: Foreground service only runs when needed (terminated state on Android โค10)
- โ Realtime updates: Instant step feedback in all running states
- โ No duplicates: Smart duplicate prevention prevents double-counting on rapid restarts
- โ OEM Compatible: Works reliably on MIUI, Samsung, and other aggressive battery optimization systems
sensors_plus Step Detection Algorithm:
- Low-pass filter for noise reduction
- Peak detection with configurable threshold
- Minimum time between steps enforcement
- Sliding Window Validation (v1.8.4+):
- Checks step rate in 2-second windows during warmup
- Prevents "shake dilution" where short bursts of shaking pass validation
- Ensures only sustained, realistic walking is counted
- Configurable via
StepDetectorConfig
โ๏ธ Advanced Configuration
For more control, use the advanced setup:
// Initialize database
await stepCounter.initializeLogging(debugLogging: true);
// Start with custom config
await stepCounter.start(
config: StepDetectorConfig(
enableOsLevelSync: true,
useForegroundServiceOnOldDevices: true,
foregroundServiceMaxApiLevel: 29, // Use service on Android โค10
),
);
// Start logging with preset
await stepCounter.startLogging(config: StepRecordConfig.walking());
// Presets: walking(), running(), sensitive(), conservative(), aggregated()
// Custom config with inactivity timeout (NEW in v1.6.0)
await stepCounter.startLogging(
config: StepRecordConfig.walking().copyWith(
inactivityTimeoutMs: 10000, // Reset after 10s of no steps
),
);
๐งช Testing
The package includes 671 automated tests covering all scenarios:
# Run all tests
flutter test
# Expected output: 00:02 +671: All tests passed!
Test Coverage
| Category | Tests |
|---|---|
| Foreground State | 100+ |
| Background State | 100+ |
| Terminated State | 100+ |
| Duplicate Prevention | 100+ |
| State Transitions | 60+ |
| API Level Tests | 50+ |
| Edge Cases | 50+ |
| Config & Parameters | 100+ |
โ Production Readiness
This package is production ready with:
- โ 671 automated tests
- โ Works on all Android versions (API 19+)
- โ OEM compatible (MIUI, Samsung, etc.)
- โ Battery efficient
- โ No duplicate step counting
- โ Handles all app states
- โ Well documented API
๐ License
MIT License - see LICENSE
Libraries
- accurate_step_counter
- Accurate step counter plugin with accelerometer-based detection