Skip to main content

pedometer_pro 0.1.0 copy "pedometer_pro: ^0.1.0" to clipboard
pedometer_pro: ^0.1.0 copied to clipboard

Pedometer & Step Detection. Get step count in a timeRange & stream live steps.

example/lib/main.dart

import 'dart:async';
import 'dart:io';

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

void main() {
  runApp(const PedometerProExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Pedometer Pro',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
        useMaterial3: true,
      ),
      home: const PedometerHomePage(),
    );
  }
}

class _DaySteps {
  const _DaySteps(this.day, this.steps);
  final DateTime day;
  final int steps;
}

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

  @override
  State<PedometerHomePage> createState() => _PedometerHomePageState();
}

class _PedometerHomePageState extends State<PedometerHomePage> {
  final Pedometer _pedometer = Pedometer();

  /// iOS stores about 7 days; Android Recording API stores about 10.
  int get _historyDays => Platform.isIOS ? 7 : 10;

  PermissionStatus? _permission;
  int? _todaySteps;
  int? _historyTotal;
  List<_DaySteps> _daily = const [];
  bool _sinceBootFallback = false;
  String? _rangeError;
  bool _loadingRange = false;

  int? _streamSteps;
  int? _streamFromToday;
  PedestrianStatus? _status;
  String? _streamError;
  String? _statusError;

  StreamSubscription<int>? _stepSub;
  StreamSubscription<int>? _stepFromSub;
  StreamSubscription<PedestrianStatus>? _statusSub;

  @override
  void dispose() {
    _stepSub?.cancel();
    _stepFromSub?.cancel();
    _statusSub?.cancel();
    super.dispose();
  }

  Future<void> _requestPermission() async {
    final status = Platform.isAndroid
        ? await Permission.activityRecognition.request()
        : await Permission.sensors.request();
    setState(() => _permission = status);
  }

  bool get _granted {
    final status = _permission;
    return status == PermissionStatus.granted ||
        status == PermissionStatus.limited;
  }

  Future<void> _loadHistory() async {
    setState(() {
      _loadingRange = true;
      _rangeError = null;
      _sinceBootFallback = false;
    });
    try {
      final now = DateTime.now();
      final todayStart = DateTime(now.year, now.month, now.day);
      final historyStart =
          todayStart.subtract(Duration(days: _historyDays - 1));

      final today = await _pedometer.getStepCount(from: todayStart, to: now);
      final historyTotal =
          await _pedometer.getStepCount(from: historyStart, to: now);

      // Sequential: Android RecordingClient is not safe to hit in parallel.
      final daily = <_DaySteps>[];
      for (var i = 0; i < _historyDays; i++) {
        final day = todayStart.subtract(Duration(days: _historyDays - 1 - i));
        final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59);
        final end = dayEnd.isAfter(now) ? now : dayEnd;
        final steps = await _pedometer.getStepCount(from: day, to: end);
        daily.add(_DaySteps(day, steps));
      }

      // Without GMS, Android TYPE_STEP_COUNTER returns the same since-boot
      // total for every day that overlaps the current boot window.
      final nonzero = daily.where((d) => d.steps > 0).toList();
      final sinceBootFallback = Platform.isAndroid &&
          nonzero.length >= 2 &&
          nonzero.every((d) => d.steps == historyTotal);

      setState(() {
        _todaySteps = today;
        _historyTotal = historyTotal;
        _daily = daily;
        _sinceBootFallback = sinceBootFallback;
      });
    } catch (e) {
      setState(() => _rangeError = e.toString());
    } finally {
      setState(() => _loadingRange = false);
    }
  }

  void _listenStreams() {
    _stepSub?.cancel();
    _stepFromSub?.cancel();
    _statusSub?.cancel();
    setState(() {
      _streamError = null;
      _statusError = null;
      _streamFromToday = null;
    });

    _stepSub = _pedometer.stepCountStream().listen(
      (steps) => setState(() => _streamSteps = steps),
      onError: (Object e) => setState(() => _streamError = e.toString()),
    );
    _statusSub = _pedometer.pedestrianStatusStream().listen(
      (status) => setState(() => _status = status),
      onError: (Object e) => setState(() => _statusError = e.toString()),
    );

    if (Platform.isIOS) {
      final todayStart = DateTime(
        DateTime.now().year,
        DateTime.now().month,
        DateTime.now().day,
      );
      _stepFromSub = _pedometer.stepCountStreamFrom(from: todayStart).listen(
        (steps) => setState(() => _streamFromToday = steps),
        onError: (Object e) => setState(() => _streamError = e.toString()),
      );
    }
  }

  String _formatDay(DateTime day) {
    final m = day.month.toString().padLeft(2, '0');
    final d = day.day.toString().padLeft(2, '0');
    return '${day.year}-$m-$d';
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Pedometer Pro')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text(
            Platform.isAndroid
                ? 'Android history comes from Health Connect (Samsung Health / Google Fit / other apps). '
                    'Galaxy phones: install Health Connect, open Samsung Health → Settings → Health Connect, '
                    'and allow this app to read steps. '
                    'play-services-fitness Local Recording is only this app\'s own log after subscribe, '
                    'not Samsung Health. If Health Connect is empty, today falls back to TYPE_STEP_COUNTER '
                    '(steps since last boot).'
                : 'iOS: getStepCount reads CoreMotion history (about 7 days).',
            style: Theme.of(context).textTheme.bodyMedium,
          ),
          const SizedBox(height: 16),
          FilledButton(
            onPressed: _requestPermission,
            child: const Text('Request permission'),
          ),
          const SizedBox(height: 8),
          Text('Permission: ${_permission?.name ?? 'not requested'}'),
          const Divider(height: 32),
          Text('History (getStepCount)',
              style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 8),
          FilledButton.tonal(
            onPressed: _granted ? _loadHistory : null,
            child: Text(
              'Load history (today + last $_historyDays days)',
            ),
          ),
          const SizedBox(height: 8),
          if (_loadingRange) const LinearProgressIndicator(),
          if (_rangeError != null)
            Text(_rangeError!, style: const TextStyle(color: Colors.red))
          else ...[
            Text('Today: ${_todaySteps ?? '-'}'),
            Text('Last $_historyDays days: ${_historyTotal ?? '-'}'),
            if (_sinceBootFallback) ...[
              const SizedBox(height: 8),
              Text(
                'This device has no Google Play Recording history. '
                'The number above is steps since last boot, not a per-day breakdown.',
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ] else if (_daily.isNotEmpty) ...[
              const SizedBox(height: 12),
              Text('Per day', style: Theme.of(context).textTheme.titleSmall),
              const SizedBox(height: 4),
              for (final entry in _daily)
                Padding(
                  padding: const EdgeInsets.symmetric(vertical: 2),
                  child: Row(
                    children: [
                      SizedBox(width: 120, child: Text(_formatDay(entry.day))),
                      Text('${entry.steps}'),
                    ],
                  ),
                ),
            ],
          ],
          const Divider(height: 32),
          Text('Live streams', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 8),
          FilledButton.tonal(
            onPressed: _granted ? _listenStreams : null,
            child: const Text('Start live streams'),
          ),
          const SizedBox(height: 8),
          Text(_streamError ?? 'Steps since boot: ${_streamSteps ?? '-'}'),
          if (Platform.isIOS)
            Text('Steps from today (stream): ${_streamFromToday ?? '-'}'),
          Text(_statusError ?? 'Pedestrian status: ${_status?.name ?? '-'}'),
          if (_permission == PermissionStatus.denied ||
              _permission == PermissionStatus.permanentlyDenied)
            TextButton(
              onPressed: openAppSettings,
              child: const Text('Open app settings'),
            ),
        ],
      ),
    );
  }
}
0
likes
160
points
126
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Pedometer & Step Detection. Get step count in a timeRange & stream live steps.

Homepage
Repository (GitHub)
View/report issues

Topics

#pedometer #step-counter #step-tracker #fit #health

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on pedometer_pro

Packages that implement pedometer_pro