accurate_step_counter 2.0.0 copy "accurate_step_counter: ^2.0.0" to clipboard
accurate_step_counter: ^2.0.0 copied to clipboard

PlatformAndroid

Production-grade step counter for Flutter Android. Uses native TYPE_STEP_COUNTER sensor via foreground service for accurate tracking in foreground, background, and terminated states. Includes SQLite l [...]

example/lib/main.dart

import 'dart:async';
import 'dart:developer' as dev;

import 'package:flutter/material.dart';
import 'package:accurate_step_counter/accurate_step_counter.dart';
import 'package:permission_handler/permission_handler.dart';

import 'verification_page.dart';
import 'warmup_test_page.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const StepCounterApp());
}

class StepCounterApp extends StatelessWidget {
  const StepCounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Accurate Step Counter',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.teal,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const StepCounterHomePage(),
    );
  }
}

class StepCounterHomePage extends StatefulWidget {
  const StepCounterHomePage({super.key});

  @override
  State<StepCounterHomePage> createState() => _StepCounterHomePageState();
}

class _StepCounterHomePageState extends State<StepCounterHomePage>
    with WidgetsBindingObserver {
  final _stepCounter = AccurateStepCounter();

  // State
  int _todaySteps = 0;
  int _liveStepCount = 0;
  int _aggregatedCount = 0;
  int _fgSteps = 0;
  int _bgSteps = 0;
  int _termSteps = 0;
  bool _isInitialized = false;
  bool _hasPermission = false;
  String _appState = 'resumed';
  String _detectorType = 'Unknown';
  bool _nativeServiceRunning = false;
  StepRuntimeState _runtimeState = StepRuntimeState.uninitialized;
  final bool _useBackgroundIsolate = true;
  final bool _performanceTracing = false;
  final List<String> _logMessages = [];

  StreamSubscription<int>? _todaySubscription;
  StreamSubscription<int>? _aggregatedSubscription;
  StreamSubscription<StepCountEvent>? _stepSubscription;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _requestPermissionAndInit();
  }

  Future<void> _requestPermissionAndInit() async {
    _log('Requesting permissions...');

    // Request activity recognition permission
    final activityStatus = await Permission.activityRecognition.request();
    _log('Activity recognition: ${activityStatus.name}');

    // Request notification permission (for foreground service on Android 13+)
    final notifStatus = await Permission.notification.request();
    _log('Notification: ${notifStatus.name}');

    if (activityStatus.isGranted) {
      setState(() => _hasPermission = true);
      await _initStepCounter();
    } else {
      _log('ERROR: Activity recognition permission denied!');
      setState(() => _hasPermission = false);
    }
  }

  Future<void> _initStepCounter() async {
    try {
      _log('Initializing step counter (production flow)...');

      await _stepCounter.initializeLogging(
        debugLogging: true,
        useBackgroundIsolate: _useBackgroundIsolate,
        performanceTracing: _performanceTracing,
      );
      _setRuntimeStateFromEngine();
      await _stepCounter.start(config: StepDetectorConfig.walking());
      _setRuntimeStateFromEngine();
      await _stepCounter.startLogging(
        config: StepRecordConfig.aggregated(
          useBackgroundIsolate: _useBackgroundIsolate,
        ),
      );
      _setRuntimeStateFromEngine();
      _log('✓ Step counter initialized (explicit startup path)');

      // Check detector type and native service status
      final isHardware = await _stepCounter.isUsingNativeDetector();
      final serviceRunning = await _stepCounter.isNativeStepServiceRunning();
      setState(() {
        _detectorType = isHardware ? 'Hardware' : 'Accelerometer';
        _nativeServiceRunning = serviceRunning;
      });
      _log('Detector: $_detectorType, Native service: $serviceRunning');
      _log(
        'Using native step service: ${_stepCounter.isUsingNativeStepService}',
      );

      // Subscribe to aggregated count stream (stored + live steps)
      _aggregatedSubscription = _stepCounter
          .watchAggregatedStepCounter()
          .listen((steps) {
            dev.log('AGGREGATED: $steps');
            _log('AGGREGATED: $steps steps');
            setState(() => _aggregatedCount = steps);
          }, onError: (e) => _log('Aggregated stream error: $e'));
      _log('✓ watchAggregatedStepCounter subscribed');

      // Subscribe to DB total for today
      _todaySubscription = _stepCounter.watchTodaySteps().listen((steps) {
        dev.log('DB TOTAL: $steps');
        setState(() => _todaySteps = steps);
        _updateSourceStats();
      }, onError: (e) => _log('Today stream error: $e'));
      _log('✓ watchTodaySteps subscribed');

      // Subscribe to raw step events from detector
      _stepSubscription = _stepCounter.stepEventStream.listen((event) {
        dev.log('RAW STEP EVENT: ${event.stepCount}');
        _log('RAW: ${event.stepCount} steps');
        setState(() => _liveStepCount = event.stepCount);
      }, onError: (e) => _log('Step stream error: $e'));
      _log('✓ stepEventStream subscribed');

      // Terminated steps callback
      _stepCounter.onTerminatedStepsDetected = (steps, from, to) {
        _log('TERMINATED SYNC: $steps steps!');
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text(
                'Synced $steps missed steps from terminated state!',
              ),
              backgroundColor: Colors.orange,
              duration: const Duration(seconds: 3),
            ),
          );
        }
      };

      // Mark as initialized AFTER streams are set up
      setState(() => _isInitialized = true);

      // Load initial data (stats by source)
      await _refreshData();
      _log('=== READY! Walk to test ===');
    } catch (e, stack) {
      _log('ERROR during initialization: $e');
      dev.log('Init error: $e\n$stack');

      // Cleanup on error
      setState(() => _isInitialized = false);
      await _cleanupStreams();
    }
  }

  Future<void> _cleanupStreams() async {
    await _stepSubscription?.cancel();
    await _aggregatedSubscription?.cancel();
    await _todaySubscription?.cancel();
    _stepSubscription = null;
    _aggregatedSubscription = null;
    _todaySubscription = null;
  }

  Future<void> _refreshData() async {
    if (!_isInitialized) return;

    final today = await _stepCounter.getTodayStepCount();
    final serviceRunning = await _stepCounter.isNativeStepServiceRunning();
    setState(() {
      _todaySteps = today;
      _nativeServiceRunning = serviceRunning;
    });
    _setRuntimeStateFromEngine();
    await _updateSourceStats();
    _log('Refreshed: $today steps today (service: $serviceRunning)');
  }

  void _setRuntimeStateFromEngine() {
    if (!mounted) return;
    setState(() {
      _runtimeState = _stepCounter.runtimeState;
    });
  }

  Future<void> _updateSourceStats() async {
    if (!_isInitialized) return;
    final fg = await _stepCounter.getStepsBySource(StepRecordSource.foreground);
    final bg = await _stepCounter.getStepsBySource(StepRecordSource.background);
    final term = await _stepCounter.getStepsBySource(
      StepRecordSource.terminated,
    );
    setState(() {
      _fgSteps = fg;
      _bgSteps = bg;
      _termSteps = term;
    });
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    _stepCounter.setAppState(state);
    setState(() => _appState = state.name);
    _setRuntimeStateFromEngine();
    _log('App state: ${state.name}');
    if (state == AppLifecycleState.resumed) {
      _refreshData();
    }
  }

  void _log(String message) {
    final now = DateTime.now();
    final time =
        '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
    dev.log('[StepCounter] $message');
    if (!mounted) return;
    setState(() {
      _logMessages.insert(0, '[$time] $message');
      if (_logMessages.length > 100) _logMessages.removeLast();
    });
  }

  Future<void> _clearAllData() async {
    await _stepCounter.clearStepLogs();
    _stepCounter.reset();
    setState(() {
      _todaySteps = 0;
      _liveStepCount = 0;
      _aggregatedCount = 0;
      _fgSteps = 0;
      _bgSteps = 0;
      _termSteps = 0;
    });
    _log('All data cleared');
  }

  Future<void> _addTestSteps() async {
    try {
      await _stepCounter.writeStepsToAggregated(
        stepCount: 10,
        fromTime: DateTime.now().subtract(const Duration(minutes: 1)),
        toTime: DateTime.now(),
        source: StepRecordSource.foreground,
      );
      _log('Added 10 test steps');
    } catch (e) {
      _log('Error: $e');
    }
  }

  Future<void> _manualTerminatedSync() async {
    if (!_isInitialized) return;
    try {
      final result = await _stepCounter.syncTerminatedSteps();
      if (result == null) {
        _log('Manual sync: no missed terminated-state steps');
      } else {
        final steps = result['missedSteps'];
        _log('Manual sync: recovered $steps steps');
      }
      await _refreshData();
    } catch (e) {
      _log('Manual sync failed: $e');
    }
  }

  Future<void> _openVerificationPage() async {
    await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const VerificationPage()),
    );
  }

  Future<void> _openWarmupTestPage() async {
    await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const WarmupTestPage()),
    );
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _cleanupStreams();
    _stepCounter.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Accurate Step Counter'),
        actions: [
          Chip(
            label: Text(_appState),
            backgroundColor: _appState == 'resumed'
                ? Colors.green
                : Colors.orange,
          ),
          const SizedBox(width: 8),
          IconButton(
            onPressed: _openVerificationPage,
            icon: const Icon(Icons.verified),
            tooltip: 'Setup Verification',
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Status Card
            Card(
              color: _hasPermission
                  ? Colors.green.shade900
                  : Colors.red.shade900,
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  children: [
                    Text(
                      _hasPermission
                          ? '✓ Permission Granted'
                          : '✗ Permission DENIED',
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text('Detector: $_detectorType'),
                    Text(
                      'Status: ${_isInitialized ? 'Active' : 'Initializing...'}',
                    ),
                    Text('Runtime: ${_runtimeState.name}'),
                    Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Icon(
                          _nativeServiceRunning
                              ? Icons.circle
                              : Icons.circle_outlined,
                          size: 10,
                          color: _nativeServiceRunning
                              ? Colors.greenAccent
                              : Colors.red,
                        ),
                        const SizedBox(width: 4),
                        Text(
                          'Native service: ${_nativeServiceRunning ? 'running' : 'stopped'}',
                        ),
                      ],
                    ),
                    Text('Isolate: ${_useBackgroundIsolate ? 'on' : 'off'}'),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Main Stats - Aggregated Count
            Card(
              color: Colors.teal.shade900,
              child: Padding(
                padding: const EdgeInsets.all(24),
                child: Column(
                  children: [
                    const Text(
                      'Today\'s Steps',
                      style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                    const SizedBox(height: 8),
                    Text(
                      '$_aggregatedCount',
                      style: const TextStyle(
                        fontSize: 72,
                        fontWeight: FontWeight.bold,
                        color: Colors.tealAccent,
                      ),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                      'Aggregated (Stored + Live)',
                      style: TextStyle(fontSize: 12, color: Colors.grey),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Source Breakdown
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'Steps by Source',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 12),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: [
                        _buildMiniStat('Database', '$_todaySteps', Colors.blue),
                        _buildMiniStat(
                          'Live',
                          '$_liveStepCount',
                          Colors.purple,
                        ),
                        _buildMiniStat('FG', '$_fgSteps', Colors.green),
                        _buildMiniStat('BG', '$_bgSteps', Colors.orange),
                        _buildMiniStat('Term', '$_termSteps', Colors.red),
                      ],
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Action Buttons
            Row(
              children: [
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _refreshData,
                    icon: const Icon(Icons.refresh),
                    label: const Text('Refresh'),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _addTestSteps,
                    icon: const Icon(Icons.add),
                    label: const Text('+10 Steps'),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: ElevatedButton.icon(
                    onPressed: _clearAllData,
                    icon: const Icon(Icons.delete),
                    label: const Text('Clear'),
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.red[700],
                    ),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 8),
            ElevatedButton.icon(
              onPressed: _openVerificationPage,
              icon: const Icon(Icons.verified_user),
              label: const Text('Run Setup Verification'),
              style: ElevatedButton.styleFrom(
                minimumSize: const Size(double.infinity, 48),
              ),
            ),
            const SizedBox(height: 8),
            ElevatedButton.icon(
              onPressed: _manualTerminatedSync,
              icon: const Icon(Icons.sync),
              label: const Text('Manual Terminated Sync'),
              style: ElevatedButton.styleFrom(
                minimumSize: const Size(double.infinity, 48),
              ),
            ),
            const SizedBox(height: 8),
            ElevatedButton.icon(
              onPressed: () {
                // Demo SmartMergeHelper
                final merged = SmartMergeHelper.mergeStepCounts(
                  sensorSteps: _liveStepCount,
                  healthConnectSteps: 0,
                  serverSteps: 0,
                  currentDisplayed: _aggregatedCount,
                );
                _log(
                  'SmartMerge: sensor=$_liveStepCount, displayed=$_aggregatedCount → merged=$merged',
                );
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('SmartMerge result: $merged steps'),
                    backgroundColor: Colors.teal,
                  ),
                );
              },
              icon: const Icon(Icons.merge),
              label: const Text('Test SmartMerge'),
              style: ElevatedButton.styleFrom(
                minimumSize: const Size(double.infinity, 48),
                backgroundColor: Colors.teal.shade800,
              ),
            ),
            const SizedBox(height: 8),
            ElevatedButton.icon(
              onPressed: _openWarmupTestPage,
              icon: const Icon(Icons.timer),
              label: const Text('Test Warmup Validation'),
              style: ElevatedButton.styleFrom(
                minimumSize: const Size(double.infinity, 48),
                backgroundColor: Colors.orange.shade800,
              ),
            ),
            const SizedBox(height: 16),

            // Step Logs Viewer
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'Step Logs (Database):',
                      style: TextStyle(fontWeight: FontWeight.bold),
                    ),
                    const SizedBox(height: 8),
                    if (_isInitialized)
                      StepLogsViewer(
                        stepCounter: _stepCounter,
                        maxHeight: 200,
                        showFilters: true,
                        showExportButton: true,
                        showDatePicker: false,
                      )
                    else
                      const Center(
                        child: Padding(
                          padding: EdgeInsets.all(16),
                          child: Text('Initializing...'),
                        ),
                      ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),

            // Debug Log
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        const Text(
                          'Debug Log:',
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                        TextButton(
                          onPressed: () => setState(() => _logMessages.clear()),
                          child: const Text('Clear'),
                        ),
                      ],
                    ),
                    const SizedBox(height: 8),
                    Container(
                      height: 200,
                      decoration: BoxDecoration(
                        color: Colors.black,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: ListView.builder(
                        padding: const EdgeInsets.all(8),
                        itemCount: _logMessages.length,
                        itemBuilder: (context, index) {
                          final msg = _logMessages[index];
                          Color color = Colors.greenAccent;
                          if (msg.contains('ERROR')) color = Colors.red;
                          if (msg.contains('RAW:')) color = Colors.yellow;
                          if (msg.contains('AGGREGATED:')) color = Colors.cyan;
                          return Text(
                            msg,
                            style: TextStyle(
                              fontFamily: 'monospace',
                              fontSize: 10,
                              color: color,
                            ),
                          );
                        },
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildMiniStat(String label, String value, [Color? color]) {
    return Column(
      children: [
        Text(
          label,
          style: TextStyle(fontSize: 11, color: color ?? Colors.grey),
        ),
        const SizedBox(height: 4),
        Text(
          value,
          style: TextStyle(
            fontSize: 18,
            color: color,
            fontWeight: FontWeight.bold,
          ),
        ),
      ],
    );
  }
}
3
likes
150
points
698
downloads

Documentation

API reference

Publisher

verified publisherrahulsha.com.np

Weekly Downloads

Production-grade step counter for Flutter Android. Uses native TYPE_STEP_COUNTER sensor via foreground service for accurate tracking in foreground, background, and terminated states. Includes SQLite logging, smart merge utilities, and warmup validation.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, path, sensors_plus, sqflite

More

Packages that depend on accurate_step_counter

Packages that implement accurate_step_counter