accurate_step_counter 1.4.0
accurate_step_counter: ^1.4.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 #
Sweet & Simple - Works in ALL States (Foreground, Background, Terminated) #
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:accurate_step_counter/accurate_step_counter.dart';
import 'package:permission_handler/permission_handler.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const StepCounterPage(),
);
}
}
class StepCounterPage extends StatefulWidget {
const StepCounterPage({super.key});
@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();
await Permission.notification.request();
// Initialize logging for persistent storage
await _stepCounter.initializeLogging(debugLogging: kDebugMode);
// Start counting with terminated state sync enabled
await _stepCounter.start(
config: StepDetectorConfig(enableOsLevelSync: true),
);
// Start auto-logging to database
await _stepCounter.startLogging(
config: StepRecordConfig.walking(),
);
// Listen to real-time step count
_stepCounter.stepEventStream.listen((event) {
setState(() => _steps = event.stepCount);
});
// Handle steps from terminated state
_stepCounter.onTerminatedStepsDetected = (steps, start, end) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Recovered $steps steps from terminated state!')),
);
};
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// CRITICAL: Track app state for proper source detection
_stepCounter.setAppState(state);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_stepCounter.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Step Counter - All States')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Real-time step count
Text(
'$_steps',
style: const TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
),
const Text('steps', style: TextStyle(fontSize: 24, color: Colors.grey)),
const SizedBox(height: 40),
// Works in foreground
const ListTile(
leading: Icon(Icons.phone_android, color: Colors.green),
title: Text('Foreground'),
subtitle: Text('Counts while app is open'),
),
// Works in background
const ListTile(
leading: Icon(Icons.layers, color: Colors.orange),
title: Text('Background'),
subtitle: Text('Counts when app is minimized'),
),
// Works in terminated state
const ListTile(
leading: Icon(Icons.power_off, color: Colors.red),
title: Text('Terminated'),
subtitle: Text('Recovers steps when app is killed & reopened'),
),
],
),
),
);
}
}
That's it! This simple example:
- β Real-time counting - Updates UI instantly
- β Foreground state - Counts while app is open
- β Background state - Continues counting when minimized
- β Terminated state - Recovers missed steps when app is killed & reopened
- β Auto-logging - Saves all steps to database with source tracking
- β
Lifecycle tracking -
setAppState()ensures proper source detection
How It Works in Each State #
| State | What Happens |
|---|---|
| π’ Foreground | Real-time updates, steps logged as foreground |
| π‘ Background | Continues counting (service on Android β€10), steps logged as background |
| π΄ Terminated | OS tracks steps, synced on relaunch (Android 11+), logged as terminated |
Try it: Open app β Walk 50 steps β Press home β Walk 50 more β Force kill β Walk 50 more β Reopen app β See all steps recovered! π
βοΈ 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 #
// Convenient date-based queries
final todaySteps = await stepCounter.getTodaySteps();
final yesterdaySteps = await stepCounter.getYesterdaySteps();
final last2Days = await stepCounter.getTodayAndYesterdaySteps();
// Custom date range
final weekSteps = await stepCounter.getStepsInRange(
DateTime.now().subtract(Duration(days: 7)),
DateTime.now(),
);
// Specific date
final jan15Steps = await stepCounter.getStepsInRange(
DateTime(2025, 1, 15),
DateTime(2025, 1, 15),
);
// All-time total (or with custom dates)
final total = await stepCounter.getTotalSteps();
final customRange = await stepCounter.getTotalSteps(
from: DateTime(2025, 1, 1),
to: DateTime.now(),
);
// 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)),
);
π Aggregated Step Counter Mode (Health Connect-like) #
The aggregated mode provides Health Connect-like behavior where steps are persistently tracked and automatically recovered across app restarts.
Key Features #
β Writes to Hive on every step event (not interval-based) β Works in foreground, background, AND terminated states β Automatically loads today's steps on app restart β Seamless aggregation (stored + live) with no double-counting β Real-time stream updates
Quick Start #
import 'package:accurate_step_counter/accurate_step_counter.dart';
import 'package:flutter/material.dart';
class AggregatedStepPage extends StatefulWidget {
@override
State<AggregatedStepPage> createState() => _AggregatedStepPageState();
}
class _AggregatedStepPageState extends State<AggregatedStepPage>
with WidgetsBindingObserver {
final _stepCounter = AccurateStepCounter();
int _aggregatedSteps = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_initStepCounter();
}
Future<void> _initStepCounter() async {
// 1. Initialize the Hive database
await _stepCounter.initializeLogging();
// 2. Start the step detector
await _stepCounter.start();
// 3. Start logging in AGGREGATED mode
await _stepCounter.startLogging(
config: StepRecordConfig.aggregated(),
);
// 4. Listen to aggregated count (stored + live)
_stepCounter.watchAggregatedStepCounter().listen((totalSteps) {
setState(() => _aggregatedSteps = totalSteps);
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Track app state for proper source detection
_stepCounter.setAppState(state);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Steps Today')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$_aggregatedSteps',
style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
),
Text('steps today'),
],
),
),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_stepCounter.dispose();
super.dispose();
}
}
How It Works #
On First Run:
User opens app β startLogging(aggregated) β Load from Hive: 0 steps
β User takes 10 steps β Stream emits: 1, 2, 3... 10
β Each step written to Hive immediately
On App Restart:
User opens app β startLogging(aggregated) β Load from Hive: 10 steps
β Stream emits: 10 (initial value)
β User takes 5 more steps β Stream emits: 11, 12, 13, 14, 15
β Hive now contains: 15 steps total β
Configuration #
// Use the aggregated preset (recommended)
await stepCounter.startLogging(
config: StepRecordConfig.aggregated(),
);
// Or customize
await stepCounter.startLogging(
config: StepRecordConfig(
enableAggregatedMode: true,
warmupDurationMs: 3000, // 3 second warmup
minStepsToValidate: 5, // Need 5 steps to pass warmup
maxStepsPerSecond: 5.0, // Max 5 steps/sec (running pace)
),
);
API Methods #
// Watch aggregated count (stored + live) in real-time
stepCounter.watchAggregatedStepCounter().listen((totalSteps) {
print('Total steps today: $totalSteps');
});
// Get current aggregated count (synchronous)
final currentTotal = stepCounter.aggregatedStepCount;
Comparison: Traditional vs Aggregated #
| Feature | Traditional | Aggregated |
|---|---|---|
| Write frequency | Every 5 seconds | Every step event |
| Persistence | Interval-based | Foreground + Background + Terminated |
| App restart | Starts from 0 | Loads today's total |
| Like Health Connect | β | β |
| Real-time accuracy | Β±5 seconds | Instant |
βοΈ Manual Step Write API #
The writeStepsToAggregated() method allows you to manually insert steps directly into the database, perfect for importing from external sources or making manual corrections.
Features #
β Instant database update β Automatic stream notification - all watchers are updated β Offset recalculation - maintains consistency β Input validation - prevents invalid data
Basic Usage #
// Write steps manually
await stepCounter.writeStepsToAggregated(
stepCount: 100,
fromTime: DateTime.now().subtract(Duration(hours: 1)),
toTime: DateTime.now(),
source: StepRecordSource.foreground,
);
// Watchers are automatically notified!
// Your UI will update instantly
Use Cases #
1. Import from Google Fit / Apple Health
// Fetch from external source
final externalSteps = await fetchFromGoogleFit();
// Write to aggregated database
await stepCounter.writeStepsToAggregated(
stepCount: externalSteps,
fromTime: startOfDay,
toTime: DateTime.now(),
source: StepRecordSource.foreground,
);
2. Manual Correction
// User manually adds steps they took
await stepCounter.writeStepsToAggregated(
stepCount: 500,
fromTime: DateTime.now().subtract(Duration(hours: 2)),
toTime: DateTime.now().subtract(Duration(hours: 1)),
);
3. Sync from Wearable
// Import from smart watch
final watchSteps = await fetchFromSmartWatch();
await stepCounter.writeStepsToAggregated(
stepCount: watchSteps,
fromTime: lastSyncTime,
toTime: DateTime.now(),
source: StepRecordSource.background,
);
Method Signature #
Future<void> writeStepsToAggregated({
required int stepCount, // Number of steps (must be > 0)
required DateTime fromTime, // Start time
DateTime? toTime, // End time (defaults to now)
StepRecordSource? source, // Source (defaults to foreground)
})
How It Works #
User calls writeStepsToAggregated(50)
β
Write StepRecord(50) to Hive
β
Recalculate offset from database
β
Emit new total to stream
β
ALL watchAggregatedStepCounter() listeners notified
β
UI updates automatically! β
Example: Before and After #
Before Manual Write:
Database (Hive): 100 steps
Live counter: 5 steps
Offset: 100
Aggregated: 100 + 5 = 105 steps
After Writing 50 Steps:
await stepCounter.writeStepsToAggregated(stepCount: 50, ...);
Database (Hive): 150 steps (100 + 50) β
Live counter: 5 steps (unchanged)
Offset: 145 (recalculated) β
Aggregated: 145 + 5 = 150 steps β
Stream emits: 150 β UI updates! β
Validation #
// β Error: Step count must be positive
await stepCounter.writeStepsToAggregated(
stepCount: 0,
fromTime: DateTime.now(),
);
// Throws: ArgumentError('Step count must be positive')
// β Error: toTime must be after fromTime
await stepCounter.writeStepsToAggregated(
stepCount: 100,
fromTime: DateTime.now(),
toTime: DateTime.now().subtract(Duration(hours: 1)),
);
// Throws: ArgumentError('toTime must be after fromTime')
Testing in Example App #
- Run the example app:
flutter run - Tap the "Aggregated" button (teal/highlighted)
- Tap "Add 50 Steps Manually" (purple button)
- Watch the aggregated count increase by 50 instantly β
ποΈ Architecture #
Overall System Flow #
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Flutter App Layer β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β AccurateStepCounter (Main API) β
β βββ stepEventStream β Real-time step events β
β βββ currentStepCount β Current session steps β
β βββ onTerminatedStepsDetected β Missed steps callback β
β βββ setAppState() β Track foreground/background β
β βββ Database Logging API β
β βββ initializeLogging() β Setup Hive database β
β βββ startLogging() β Auto-log with warmup validation β
β βββ getTotalSteps() β Query aggregate β
β βββ getStepsBySource() β Query by source type β
β βββ watchTotalSteps() β Real-time database stream β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Hive Database (Local Storage) β
β βββ StepRecord (Model) β {stepCount, fromTime, toTime} β
β βββ StepRecordSource β foreground | background | terminatedβ
β βββ StepRecordStore (Service) β CRUD operations + streams β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β NativeStepDetector (Dart Side) β
β βββ MethodChannel β Commands (start, stop, reset) β
β βββ EventChannel β Step events from native β
βββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββ
β
Platform Channel (MethodChannel + EventChannel)
β
βββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββ
β Android Native Layer (Kotlin) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β AccurateStepCounterPlugin β
β βββ NativeStepDetector.kt β Sensor management + event streaming β
β βββ StepCounterForegroundService.kt β Background service (API β€29) β
β βββ SharedPreferences β State persistence (OS-level sync) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Android Sensor Framework β
β βββ TYPE_STEP_DETECTOR β Hardware step sensor (preferred) β
β βββ TYPE_STEP_COUNTER β OS-level counter (for sync) β
β βββ TYPE_ACCELEROMETER β Fallback (software algorithm) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
App State Handling Architecture #
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β App Lifecycle States β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π’ FOREGROUND (AppLifecycleState.resumed)
βββββββββββββββββββββββββββββββββββββββββββββββ
β App Active & Visible β
β βββ NativeStepDetector active β
β βββ Real-time UI updates β
β βββ Steps logged as: foreground β
β βββ Best accuracy & responsiveness β
βββββββββββββββββββββββββββββββββββββββββββββββ
β Press Home / Switch App
π‘ BACKGROUND (AppLifecycleState.paused)
βββββββββββββββββββββββββββββββββββββββββββββββ
β App Minimized but Running β
β β
β Android 11+ (API 30+) β
β βββ Native sensor continues automatically β
β βββ No notification needed β
β βββ Steps logged as: background β
β β
β Android β€10 (API β€29) β
β βββ Foreground Service activated β
β βββ Persistent notification shown β
β βββ WakeLock keeps CPU active β
β βββ Steps logged as: background β
βββββββββββββββββββββββββββββββββββββββββββββββ
β Force Stop / OS Kills App
π΄ TERMINATED (App Killed)
βββββββββββββββββββββββββββββββββββββββββββββββ
β App Completely Stopped β
β β
β Android 11+ (API 30+) β
β βββ OS continues via TYPE_STEP_COUNTER β
β βββ Steps tracked by Android system β
β βββ On relaunch: sync missed steps β
β βββ Steps logged as: terminated β
β β
β Android β€10 (API β€29) β
β βββ Foreground Service prevents death β
β βββ Service survives Activity destruction β
β βββ No true terminated state β
βββββββββββββββββββββββββββββββββββββββββββββββ
β User Reopens App
π’ FOREGROUND (Back to resumed)
βββββββββββββββββββββββββββββββββββββββββββββββ
β App Relaunched β
β βββ onTerminatedStepsDetected fires β
β βββ Missed steps synced to database β
β βββ Resume normal counting β
βββββββββββββββββββββββββββββββββββββββββββββββ
Sensor Selection & Fallback Strategy #
βββββββββββββββββββββββ
β App Starts β
ββββββββββββ¬βββββββββββ
β
ββββββββββββΌβββββββββββ
β Check Android API β
ββββββββββββ¬βββββββββββ
β
ββββββββββββββββββ΄βββββββββββββββββ
β β
ββββββββββΌβββββββββ ββββββββββΌβββββββββ
β API β€ 29 β β API β₯ 30 β
β (Android β€10) β β (Android 11+) β
ββββββββββ¬βββββββββ ββββββββββ¬βββββββββ
β β
ββββββββββββΌβββββββββββ βββββββββββΌβββββββββββ
β Foreground Service β β Native Detection β
β - Persistent notify β β + OS-level sync β
β - WakeLock active β β - No notification β
β - Keeps app alive β β - Better battery β
ββββββββββββ¬βββββββββββ βββββββββββ¬βββββββββββ
β β
ββββββββββββββββββ¬βββββββββββββββββ
β
ββββββββββββΌβββββββββββββββ
β Check TYPE_STEP_DETECTORβ
β (Hardware Sensor) β
ββββββββββββ¬βββββββββββββββ
β
ββββββββββββββββββ΄βββββββββββββββββ
β β
ββββββββββΌβββββββββ ββββββββββΌβββββββββ
β β
Available β β β Not Found β
ββββββββββ¬βββββββββ ββββββββββ¬βββββββββ
β β
ββββββββββββΌβββββββββββ βββββββββββΌβββββββββββ
β Hardware Detection β β Accelerometer β
β ββ Event-driven β β + Software Algo β
β ββ Best accuracy β β ββ Low-pass filter β
β ββ Battery efficientβ β ββ Peak detection β
β ββ Android optimizedβ β ββ Configurable β
βββββββββββββββββββββββ ββββββββββββββββββββββ
Data Flow: Step Detection β Database #
ββββββββββββββββββββ
β User Walks β π£
ββββββββββ¬ββββββββββ
β
ββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Android Sensor (TYPE_STEP_DETECTOR or ACCELEROMETER) β
ββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββΌββββββββββ
β NativeDetector β (Kotlin)
β - Filters noise β
β - Emits events β
ββββββββββ¬ββββββββββ
β EventChannel
ββββββββββΌββββββββββ
β AccurateStep β (Dart)
β Counter β
β - stepCount++ β
ββββββββββ¬ββββββββββ
β
ββββββΌβββββ
β Warmup? β
ββββββ¬βββββ
β
ββββββΌβββββββββββββββββββββββββββββ
β YES: Buffer steps β
β ββ Wait for warmup duration β
β ββ Validate step count β
β ββ Validate step rate β
β ββ Log if validated β
ββββββ¬βββββββββββββββββββββββββββββ
β
ββββββΌβββββββββββββββββββββββββββββ
β NO: Normal logging β
β ββ Check interval elapsed β
β ββ Validate step rate β
β ββ Log to database β
ββββββ¬βββββββββββββββββββββββββββββ
β
ββββββββββΌββββββββββ
β Determine β
β Source Type β
β ββ Foreground β
β ββ Background β
β ββ Terminated β
ββββββββββ¬ββββββββββ
β
ββββββββββΌββββββββββ
β Hive Database β πΎ
β StepRecord β
β - stepCount β
β - fromTime β
β - toTime β
β - source β
β - confidence β
ββββββββββββββββββββ
Terminated State Sync Flow (Android 11+) #
βββββββββββββββββββ
β App Running β
β Walk 100 steps β
ββββββββββ¬βββββββββ
β
β Save state to SharedPreferences:
β - lastStepCount: 1000 (OS counter)
β - timestamp: 10:00 AM
β
ββββββββββΌβββββββββ
β App Killed β β (Force stop or OS kills it)
ββββββββββ¬βββββββββ
β
ββββββββββΌβββββββββ
β User Walks β π£ Walk 50 more steps
β (OS counting) β OS step counter: 1000 β 1050
ββββββββββ¬βββββββββ
β
ββββββββββΌβββββββββ
β App Relaunched β π
ββββββββββ¬βββββββββ
β
ββββββββββΌββββββββββββββββββββββββββββββββββ
β Sync Process (automatic) β
β 1. Read current OS count: 1050 β
β 2. Read saved count: 1000 β
β 3. Calculate missed: 1050 - 1000 = 50 β
β 4. Validate: β
β β Positive number β
β β < 50,000 (max reasonable) β
β β Step rate < 3 steps/sec β
β β Time not negative β
β 5. onTerminatedStepsDetected(50, ...) β
β 6. Log to database as: terminated β
β 7. Save new baseline: 1050 β
ββββββββββββββββββββββββββββββββββββββββββββ
π± 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 |
π§ͺ Testing & Verification #
Quick Setup Verification #
Run this code to verify everything is configured correctly:
Future<void> verifySetup() async {
final stepCounter = AccurateStepCounter();
// 1. Check permission
final hasPermission = await stepCounter.hasActivityRecognitionPermission();
print('β Permission: $hasPermission');
// 2. Initialize logging
await stepCounter.initializeLogging(debugLogging: true);
print('β Logging initialized: ${stepCounter.isLoggingInitialized}');
// 3. Start counter
await stepCounter.start();
print('β Started: ${stepCounter.isStarted}');
// 4. Check detector
final isHardware = await stepCounter.isUsingNativeDetector();
print('β Hardware detector: $isHardware');
// 5. Enable logging
await stepCounter.startLogging(config: StepRecordConfig.walking());
print('β Logging enabled: ${stepCounter.isLoggingEnabled}');
}
Real-Life Test Scenarios #
The package includes 7 comprehensive test scenarios covering all app states:
- Morning Walk - Foreground state counting
- Background Mode - Shopping while app is backgrounded
- Terminated State Recovery - App killed and relaunched
- All-Day Tracking - Mixed states throughout the day
- Running Workout - High-intensity activity
- Device Reboot - Handling sensor resets
- Permission Handling - Edge cases and failures
See TESTING_SCENARIOS.md for detailed testing instructions.
Automated Test Runner #
Use the included test script for easy testing:
chmod +x test_runner.sh
./test_runner.sh
The script will:
- β Check device connection
- β Build and install the example app
- β Grant required permissions
- β Run scenario tests
- β Monitor logs in real-time
Manual Testing Checklist #
[ ] Foreground counting (100 steps, Β±5% accuracy)
[ ] Background counting (proper source tracking)
[ ] Terminated state sync (missed steps recovered)
[ ] Warmup validation (prevents false positives)
[ ] Real-time stream updates
[ ] Database logging persists
[ ] Notification shows on Android β€10
[ ] No crashes or errors
π Quick Reference #
Essential API Calls #
// Basic Setup
final stepCounter = AccurateStepCounter();
await stepCounter.initializeLogging(debugLogging: kDebugMode);
await stepCounter.start(config: StepDetectorConfig(enableOsLevelSync: true));
await stepCounter.startLogging(config: StepRecordConfig.walking());
// App Lifecycle (CRITICAL for proper source tracking)
void didChangeAppLifecycleState(AppLifecycleState state) {
stepCounter.setAppState(state);
}
// Real-time Step Count
stepCounter.stepEventStream.listen((event) {
print('Steps: ${event.stepCount}');
});
// Terminated State Callback
stepCounter.onTerminatedStepsDetected = (steps, start, end) {
print('Recovered $steps steps from $start to $end');
};
// Query Database - Convenient date methods
final todaySteps = await stepCounter.getTodaySteps();
final yesterdaySteps = await stepCounter.getYesterdaySteps();
final last2Days = await stepCounter.getTodayAndYesterdaySteps();
final weekSteps = await stepCounter.getStepsInRange(
DateTime.now().subtract(Duration(days: 7)),
DateTime.now(),
);
// By source
final fgSteps = await stepCounter.getStepsBySource(StepRecordSource.foreground);
final bgSteps = await stepCounter.getStepsBySource(StepRecordSource.background);
final termSteps = await stepCounter.getStepsBySource(StepRecordSource.terminated);
// Real-time Database Stream
stepCounter.watchTotalSteps().listen((total) {
print('Total: $total');
});
// Cleanup
await stepCounter.stop();
await stepCounter.dispose();
Configuration Presets Quick Pick #
| Activity | Detector Config | Logging Config |
|---|---|---|
| Casual Walking | StepDetectorConfig.walking() |
StepRecordConfig.walking() |
| Running/Jogging | StepDetectorConfig.running() |
StepRecordConfig.running() |
| High Sensitivity | StepDetectorConfig.sensitive() |
StepRecordConfig.sensitive() |
| Strict Accuracy | StepDetectorConfig.conservative() |
StepRecordConfig.conservative() |
| Raw Data | Default | StepRecordConfig.noValidation() |
Platform Behavior Matrix #
| Feature | Android 11+ | Android β€10 |
|---|---|---|
| Foreground Counting | β Native detector | β Native detector |
| Background Counting | β Automatic | β Foreground service |
| Notification | β None | β Required |
| Terminated Recovery | β OS-level sync | β οΈ Service prevents termination |
| Battery Impact | π’ Low | π‘ Medium |
| Setup Required | Minimal | Notification permission |
Common Patterns #
Pattern 1: Basic Real-Time Counter
final counter = AccurateStepCounter();
await counter.start();
counter.stepEventStream.listen((e) => print(e.stepCount));
Pattern 2: Persistent All-Day Tracking
final counter = AccurateStepCounter();
await counter.initializeLogging(debugLogging: kDebugMode);
await counter.start(config: StepDetectorConfig(enableOsLevelSync: true));
await counter.startLogging(config: StepRecordConfig.walking());
// Track app state in didChangeAppLifecycleState
counter.setAppState(state);
// Query anytime
final total = await counter.getTotalSteps();
Pattern 3: Activity Tracking with Source Breakdown
final counter = AccurateStepCounter();
await counter.initializeLogging(debugLogging: true);
await counter.start(config: StepDetectorConfig(enableOsLevelSync: true));
await counter.startLogging(config: StepRecordConfig.walking());
// Get breakdown
final stats = await counter.getStepStats();
print('Foreground: ${stats['foregroundSteps']}');
print('Background: ${stats['backgroundSteps']}');
print('Terminated: ${stats['terminatedSteps']}');
Troubleshooting Quick Fixes #
| Problem | Solution |
|---|---|
| No steps counted | Check ACTIVITY_RECOGNITION permission granted |
| Stops in background (Android β€10) | Notification permission granted? Check battery optimization |
| No terminated sync | Set enableOsLevelSync: true in config |
| Database empty | Called initializeLogging() and startLogging()? |
| No real-time updates | Subscribed to stepEventStream? |
| Wrong source tracking | Implemented didChangeAppLifecycleState with setAppState()? |
Debug Commands #
# View all logs
adb logcat -s AccurateStepCounter NativeStepDetector StepSync
# Clear logs and watch
adb logcat -c && adb logcat -s AccurateStepCounter
# Check sensor availability
adb shell dumpsys sensorservice | grep -i step
π License #
MIT License - see LICENSE
π Links #
- π¦ pub.dev
- π GitHub
- π Changelog
- π Issues
- π§ͺ Testing Guide
Made with β€οΈ for the Flutter community