Accurate Step Counter

pub package License: MIT Production Ready

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.

โœจ Features

  • ๐ŸŽฏ 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 Support

Platform Status Note
Android โœ… Full support (API 24+) Native TYPE_STEP_COUNTER + foreground service
iOS โŒ Not supported

๐Ÿš€ Quick Start

1. Install

dependencies:
  accurate_step_counter: ^2.0.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. Use It

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');
});

๐Ÿ“– 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 {
    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)),
      ),
    );
  }
}

๐Ÿ”€ Smart Merge (NEW in v2.0.0)

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

๐Ÿ—๏ธ Architecture

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

๐Ÿ”ง API Reference

Core Methods

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()

Aggregated Mode

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

Smart Merge

Method Description
SmartMergeHelper.mergeStepCounts() Merge sensor + HC + server (max of all)
SmartMergeHelper.mergeSensorAndHealth() Simplified merge (sensor + HC only)

Debug

Method Description
StepLogsViewer Widget for viewing step logs with filters
getStepLogs() Get all step log entries
getStepStats() Get statistics (totals by source, averages)

โš™๏ธ Configuration

// 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());

๐Ÿ”’ Reliability Features

  • 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

๐Ÿ“ฑ Low-End Device Support

// Background isolate moves all DB work off the main thread
await stepCounter.initializeLogging(useBackgroundIsolate: true);
await stepCounter.startLogging(config: StepRecordConfig.lowEndDevice());

๐Ÿ“„ License

MIT License โ€” see LICENSE

Libraries

accurate_step_counter
Accurate step counter plugin with accelerometer-based detection