accurate_step_counter 1.3.0
accurate_step_counter: ^1.3.0 copied to clipboard
A highly accurate step counter plugin using accelerometer-based detection with low-pass filtering and peak detection. Includes Hive database logging with warmup validation. Supports foreground, backgr [...]
Accurate Step Counter #
A highly accurate Flutter plugin for step counting using native Android TYPE_STEP_DETECTOR sensor with accelerometer fallback. Includes local Hive database logging with warmup validation. Zero external dependencies. Designed for reliability across foreground, background, and terminated app states.
β¨ Features #
| Feature | Description |
|---|---|
| π― Native Detection | Uses Android's hardware-optimized TYPE_STEP_DETECTOR sensor |
| π Accelerometer Fallback | Software algorithm for devices without step detector |
| πΎ Hive Logging | Local persistent storage with source tracking (foreground/background/terminated) |
| π₯ Warmup Validation | Buffer steps during warmup, validate walking before logging |
| π¦ Zero Dependencies | Only requires Flutter SDK + Hive |
| π Battery Efficient | Event-driven, not polling-based |
| π± All App States | Foreground, background, and terminated state support |
| βοΈ Configurable | Presets for walking/running + custom parameters |
π± Platform Support #
| Platform | Status |
|---|---|
| Android | β Full support (API 19+) |
| iOS | β Not supported |
Note: This is an Android-only package. It won't crash on iOS but step detection won't work.
π Quick Start #
1. Install #
dependencies:
accurate_step_counter: ^1.3.0
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. Request Runtime Permission #
import 'package:permission_handler/permission_handler.dart';
// Request activity recognition (required)
await Permission.activityRecognition.request();
// Request notification (for Android 13+ foreground service)
await Permission.notification.request();
4. Start Counting! #
import 'package:accurate_step_counter/accurate_step_counter.dart';
final stepCounter = AccurateStepCounter();
// Listen to step events
stepCounter.stepEventStream.listen((event) {
print('Steps: ${event.stepCount}');
});
// Start counting
await stepCounter.start();
// Stop when done
await stepCounter.stop();
// Clean up
await stepCounter.dispose();
π Complete Example #
import 'package:flutter/material.dart';
import 'package:accurate_step_counter/accurate_step_counter.dart';
import 'dart:async';
class StepCounterScreen extends StatefulWidget {
@override
State<StepCounterScreen> createState() => _StepCounterScreenState();
}
class _StepCounterScreenState extends State<StepCounterScreen> {
final _stepCounter = AccurateStepCounter();
StreamSubscription<StepCountEvent>? _subscription;
int _steps = 0;
bool _isRunning = false;
@override
void initState() {
super.initState();
_subscription = _stepCounter.stepEventStream.listen((event) {
setState(() => _steps = event.stepCount);
});
}
Future<void> _toggleTracking() async {
if (_isRunning) {
await _stepCounter.stop();
} else {
await _stepCounter.start();
}
setState(() => _isRunning = !_isRunning);
}
@override
void dispose() {
_subscription?.cancel();
_stepCounter.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Step Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$_steps', style: const TextStyle(fontSize: 80, fontWeight: FontWeight.bold)),
const Text('steps', style: TextStyle(fontSize: 24, color: Colors.grey)),
const SizedBox(height: 40),
ElevatedButton.icon(
onPressed: _toggleTracking,
icon: Icon(_isRunning ? Icons.stop : Icons.play_arrow),
label: Text(_isRunning ? 'Stop' : 'Start'),
),
TextButton(
onPressed: () {
_stepCounter.reset();
setState(() => _steps = 0);
},
child: const Text('Reset'),
),
],
),
),
);
}
}
βοΈ Configuration #
Presets #
// For walking (default)
await stepCounter.start(config: StepDetectorConfig.walking());
// For running (more sensitive, faster detection)
await stepCounter.start(config: StepDetectorConfig.running());
// Sensitive mode (may have false positives)
await stepCounter.start(config: StepDetectorConfig.sensitive());
// Conservative mode (fewer false positives)
await stepCounter.start(config: StepDetectorConfig.conservative());
Custom Configuration #
await stepCounter.start(
config: StepDetectorConfig(
threshold: 1.2, // Movement threshold (higher = less sensitive)
filterAlpha: 0.85, // Smoothing factor (0.0 - 1.0)
minTimeBetweenStepsMs: 250, // Minimum ms between steps
enableOsLevelSync: true, // Sync with OS step counter
// Foreground service options
useForegroundServiceOnOldDevices: true,
foregroundServiceMaxApiLevel: 29, // API level threshold (default: 29 = Android 10)
foregroundNotificationTitle: 'Step Tracker',
foregroundNotificationText: 'Counting your steps...',
),
);
Configuration Parameters #
| Parameter | Default | Description |
|---|---|---|
threshold |
1.0 | Movement threshold for step detection |
filterAlpha |
0.8 | Low-pass filter smoothing (0.0-1.0) |
minTimeBetweenStepsMs |
200 | Minimum time between steps |
enableOsLevelSync |
true | Sync with OS step counter |
useForegroundServiceOnOldDevices |
true | Use foreground service on older Android |
foregroundServiceMaxApiLevel |
29 | Max API level for foreground service (29=Android 10, 31=Android 12, etc.) |
foregroundNotificationTitle |
"Step Counter" | Notification title |
foregroundNotificationText |
"Tracking your steps..." | Notification text |
π§ API Reference #
AccurateStepCounter #
final stepCounter = AccurateStepCounter();
// Properties
stepCounter.stepEventStream // Stream<StepCountEvent>
stepCounter.currentStepCount // int
stepCounter.isStarted // bool
stepCounter.isUsingForegroundService // bool
stepCounter.currentConfig // StepDetectorConfig?
// Methods
await stepCounter.start({config}); // Start detection
await stepCounter.stop(); // Stop detection
stepCounter.reset(); // Reset count to zero
await stepCounter.dispose(); // Clean up resources
// Check sensor type
final isHardware = await stepCounter.isUsingNativeDetector();
// Terminated state sync (automatic, but can be manual)
stepCounter.onTerminatedStepsDetected = (steps, startTime, endTime) {
print('Synced $steps missed steps');
};
StepCountEvent #
final event = StepCountEvent(stepCount: 100, timestamp: DateTime.now());
event.stepCount // int - Total steps since start()
event.timestamp // DateTime - When step was detected
πΎ Hive Step Logging #
Setup #
import 'package:flutter/foundation.dart';
import 'package:accurate_step_counter/accurate_step_counter.dart';
final stepCounter = AccurateStepCounter();
// Initialize logging database
// debugLogging: kDebugMode = only show console logs in debug builds
await stepCounter.initializeLogging(debugLogging: kDebugMode);
// Start counting
await stepCounter.start();
// Start logging with a preset
await stepCounter.startLogging(config: StepLoggingConfig.walking());
Debug Logging Parameter #
Control console output with the debugLogging parameter:
// No console output (default - recommended for production)
await stepCounter.initializeLogging(debugLogging: false);
// Always show console logs
await stepCounter.initializeLogging(debugLogging: true);
// Only in debug builds (recommended)
await stepCounter.initializeLogging(debugLogging: kDebugMode);
Console output examples when debugLogging: true:
AccurateStepCounter: Logging database initialized
AccurateStepCounter: Warmup started
AccurateStepCounter: Warmup validated - 15 steps at 1.87/s
AccurateStepCounter: Logged 15 warmup steps (source: StepLogSource.foreground)
Logging Presets #
| Preset | Warmup | Min Steps | Max Rate | Use Case |
|---|---|---|---|---|
walking() |
5s | 8 | 3/s | Casual walks |
running() |
3s | 10 | 5/s | Jogging/running |
sensitive() |
0s | 3 | 6/s | Quick detection |
conservative() |
10s | 15 | 2.5/s | Strict accuracy |
noValidation() |
0s | 1 | 100/s | Raw data |
// Use a preset
await stepCounter.startLogging(config: StepLoggingConfig.walking());
// Custom configuration
await stepCounter.startLogging(
config: StepLoggingConfig(
logIntervalMs: 5000, // Log every 5 seconds
warmupDurationMs: 8000, // 8 second warmup period
minStepsToValidate: 10, // Need 10+ steps to confirm walking
maxStepsPerSecond: 4.0, // Reject rates above 4/second
),
);
Complete Example #
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:accurate_step_counter/accurate_step_counter.dart';
class StepTrackerPage extends StatefulWidget {
@override
State<StepTrackerPage> createState() => _StepTrackerPageState();
}
class _StepTrackerPageState extends State<StepTrackerPage>
with WidgetsBindingObserver {
final _stepCounter = AccurateStepCounter();
int _totalSteps = 0;
int _foregroundSteps = 0;
int _backgroundSteps = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_init();
}
Future<void> _init() async {
// 1. Initialize logging (with debug output in debug builds)
await _stepCounter.initializeLogging(debugLogging: kDebugMode);
// 2. Start step detection
await _stepCounter.start();
// 3. Start logging with walking preset
await _stepCounter.startLogging(config: StepLoggingConfig.walking());
// 4. Listen to total steps in real-time
_stepCounter.watchTotalSteps().listen((total) {
setState(() => _totalSteps = total);
});
// 5. Handle terminated state sync
_stepCounter.onTerminatedStepsDetected = (steps, from, to) {
print('Synced $steps steps from terminated state');
};
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Track app state for proper source detection
_stepCounter.setAppState(state);
}
Future<void> _refreshStats() async {
final fg = await _stepCounter.getStepsBySource(StepLogSource.foreground);
final bg = await _stepCounter.getStepsBySource(StepLogSource.background);
setState(() {
_foregroundSteps = fg;
_backgroundSteps = bg;
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_stepCounter.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Total: $_totalSteps steps'),
Text('Foreground: $_foregroundSteps'),
Text('Background: $_backgroundSteps'),
ElevatedButton(
onPressed: _refreshStats,
child: Text('Refresh Stats'),
),
],
);
}
}
Query API #
// Aggregate total
final total = await stepCounter.getTotalSteps();
// Today's steps
final today = DateTime.now();
final startOfDay = DateTime(today.year, today.month, today.day);
final todaySteps = await stepCounter.getTotalSteps(from: startOfDay);
// By source
final fgSteps = await stepCounter.getStepsBySource(StepLogSource.foreground);
final bgSteps = await stepCounter.getStepsBySource(StepLogSource.background);
final termSteps = await stepCounter.getStepsBySource(StepLogSource.terminated);
// Get all logs
final logs = await stepCounter.getStepLogs();
// Statistics
final stats = await stepCounter.getStepStats();
// Returns: {totalSteps, entryCount, averagePerEntry, averagePerDay,
// foregroundSteps, backgroundSteps, terminatedSteps}
Real-Time Streams #
// Watch total steps
stepCounter.watchTotalSteps().listen((total) {
print('Total: $total');
});
// Watch all logs with filter
stepCounter.watchStepLogs(source: StepLogSource.foreground).listen((logs) {
for (final log in logs) {
print('${log.stepCount} steps at ${log.toTime}');
}
});
Data Management #
// Clear all logs
await stepCounter.clearStepLogs();
// Delete logs older than 30 days
await stepCounter.deleteStepLogsBefore(
DateTime.now().subtract(Duration(days: 30)),
);
ποΈ Architecture #
Overall Flow #
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Flutter App β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β AccurateStepCounter β
β βββ stepEventStream (real-time steps) β
β βββ currentStepCount β
β βββ onTerminatedStepsDetected (missed steps callback) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β NativeStepDetector (Dart) β
β βββ MethodChannel (commands) β
β βββ EventChannel (step events) β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
Platform Channel
β
ββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β Android Native (Kotlin) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β AccurateStepCounterPlugin β
β βββ NativeStepDetector.kt (sensor handling) β
β βββ StepCounterForegroundService.kt (Android β€10) β
β βββ SharedPreferences (state persistence) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Android Sensors β
β βββ TYPE_STEP_DETECTOR (primary - hardware) β
β βββ TYPE_ACCELEROMETER (fallback - software) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Step Detection Priority #
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Check: TYPE_STEP_DETECTOR β
β (Hardware Sensor) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββ
β
βββββββββββββΌββββββββββββ
β Available? β
βββββββββββββ¬ββββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β β
βββββββΌββββββ ββββββββΌβββββββ
β YES β β NO β
βββββββ¬ββββββ ββββββββ¬βββββββ
β β
βββββββββββΌββββββββββ βββββββββββΌββββββββββ
β Hardware Step β β Accelerometer β
β Detection β β + Algorithm β
β β β β
β β’ Best accuracy β β β’ Low-pass filterβ
β β’ Battery saving β β β’ Peak detection β
β β’ Event-driven β β β’ Configurable β
βββββββββββββββββββββ βββββββββββββββββββββ
π± App State Coverage #
How Each State is Handled #
| App State | Android 11+ (API 30+) | Android β€10 (API β€29) |
|---|---|---|
| π’ Foreground | Native TYPE_STEP_DETECTOR |
Native TYPE_STEP_DETECTOR |
| π‘ Background | Native detection continues | Foreground Service keeps counting |
| π΄ Terminated | OS sync on app relaunch | Foreground Service prevents termination |
| π’ Notification | β None (not needed) | β Shows (required by Android) |
Important: The persistent notification only appears on Android devices with API level β€
foregroundServiceMaxApiLevel(default: 29 = Android 10). On newer devices, no notification is shown because the native step detector works without needing a foreground service.
Detailed State Behavior #
π’ Foreground State
App Active β NativeStepDetector β TYPE_STEP_DETECTOR β EventChannel β Flutter UI
- Real-time step counting with immediate updates
- Hardware-optimized detection
- Full access to all sensors
π‘ Background State
Android 11+:
App Minimized β Native detection continues β Steps buffered β UI updates when resumed
Android β€10:
App Minimized β Foreground Service starts β Persistent notification shown
β
Keeps CPU active via WakeLock
β
Steps counted continuously
β
Results polled every 500ms
π΄ Terminated State
Android 11+:
App Killed β OS continues counting via TYPE_STEP_COUNTER
β
App Relaunched
β
Compare saved count with current OS count
β
Calculate missed steps
β
Trigger onTerminatedStepsDetected callback
Android β€10:
Foreground Service prevents true termination
β
Service continues counting even if Activity destroyed
β
No steps are ever missed
Terminated State Sync (Android 11+) #
// Automatic sync happens on start(), but you can handle it:
stepCounter.onTerminatedStepsDetected = (missedSteps, startTime, endTime) {
print('You walked $missedSteps steps while app was closed!');
print('From: $startTime to $endTime');
// Optionally save to database or sync to server
saveToDatabase(missedSteps, startTime, endTime);
};
π Battery & Performance #
| Metric | Value |
|---|---|
| Detection Method | Event-driven (not polling) |
| CPU Usage | Minimal (~1-2%) |
| Battery Impact | Low (uses hardware sensor) |
| Memory | ~2-5 MB |
| Foreground Service Battery | Moderate (only Android β€10) |
π Debugging #
View Logs #
# All plugin logs
adb logcat -s AccurateStepCounter NativeStepDetector StepSync
# Only step events
adb logcat -s NativeStepDetector
Check Sensor Availability #
final isHardware = await stepCounter.isUsingNativeDetector();
print('Using hardware step detector: $isHardware');
β Troubleshooting #
| Issue | Solution |
|---|---|
| Steps not detected | Check ACTIVITY_RECOGNITION permission is granted |
| Inaccurate counts | Try adjusting threshold parameter |
| Stops in background | Enable foreground service or check battery optimization |
| No notification (Android β€10) | Grant notification permission |
π License #
MIT License - see LICENSE
π Links #
- π¦ pub.dev
- π GitHub
- π Changelog
- π Issues
Made with β€οΈ for the Flutter community