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

PlatformAndroid

A highly accurate step counter plugin using accelerometer-based detection with low-pass filtering and peak detection. Includes SQLite database logging with warmup validation. Supports foreground, back [...]

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

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

      // Simple one-line initialization with debug logging
      await _stepCounter.initSteps(debugLogging: true);
      _log('✓ Step counter initialized');

      // Check detector type
      final isHardware = await _stepCounter.isUsingNativeDetector();
      setState(() => _detectorType = isHardware ? 'Hardware' : 'Accelerometer');
      _log('Detector type: $_detectorType');

      // 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();
    setState(() => _todaySteps = today);
    await _updateSourceStats();
    _log('Refreshed: $today steps today');
  }

  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);
    _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');
    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> _openVerificationPage() async {
    await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const VerificationPage()),
    );
  }

  @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...'}',
                    ),
                  ],
                ),
              ),
            ),
            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: 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
120
points
1.03k
downloads

Publisher

verified publisherrahulsha.com.np

Weekly Downloads

A highly accurate step counter plugin using accelerometer-based detection with low-pass filtering and peak detection. Includes SQLite database logging with warmup validation. Supports foreground, background, and terminated state tracking.

Repository (GitHub)
View/report issues

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

flutter, path, sensors_plus, sqflite

More

Packages that depend on accurate_step_counter

Packages that implement accurate_step_counter