android_sleep_tracker 0.0.1 copy "android_sleep_tracker: ^0.0.1" to clipboard
android_sleep_tracker: ^0.0.1 copied to clipboard

PlatformAndroid

Flutter API over Android's native sleep tracking (Google Play Services Activity Recognition Sleep API). Android-only, no UI, no health scoring.

example/lib/main.dart

import 'dart:async';

import 'package:android_sleep_tracker/android_sleep_tracker.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorSchemeSeed: const Color(0xFF5B4FE9),
        useMaterial3: true,
      ),
      home: const SleepTrackerPage(),
    );
  }
}

/// What the UI shows for the last start/stop/refresh attempt.
enum _Status { idle, unsupported, permissionDenied, error }

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

  @override
  State<SleepTrackerPage> createState() => _SleepTrackerPageState();
}

class _SleepTrackerPageState extends State<SleepTrackerPage> {
  final _tracker = AndroidSleepTracker.instance;

  bool _isTracking = false;
  _Status _status = _Status.idle;
  String? _errorDetail;
  bool _busy = false;

  final List<SleepApiEvent> _liveEvents = [];
  StreamSubscription<SleepApiEvent>? _eventSubscription;
  static const _maxLiveEvents = 20;

  @override
  void initState() {
    super.initState();
    _refresh();
    // No polling: startTracking() (below) registers with the Sleep API,
    // and this stream just relays whatever it pushes, live.
    _eventSubscription = _tracker.sleepDataStream.listen(
      _onSleepEvent,
      onError: (Object error) {
        debugPrint('[android_sleep_tracker] sleepDataStream error: $error');
      },
    );
  }

  @override
  void dispose() {
    _eventSubscription?.cancel();
    super.dispose();
  }

  void _onSleepEvent(SleepApiEvent event) {
    debugPrint('[android_sleep_tracker] sleepDataStream event: $event');
    if (!mounted) return;
    setState(() {
      _liveEvents.insert(0, event);
      if (_liveEvents.length > _maxLiveEvents) {
        _liveEvents.removeRange(_maxLiveEvents, _liveEvents.length);
      }
    });
  }

  Future<void> _refresh() async {
    final tracking = await _tracker.isTracking();
    debugPrint('[android_sleep_tracker] isTracking=$tracking');
    if (!mounted) return;
    setState(() => _isTracking = tracking);
  }

  Future<void> _start() async {
    setState(() {
      _busy = true;
      _status = _Status.idle;
      _errorDetail = null;
    });
    try {
      await _tracker.startTracking();
    } on SleepTrackerUnsupportedException {
      setState(() => _status = _Status.unsupported);
    } on SleepTrackerPermissionDeniedException {
      setState(() => _status = _Status.permissionDenied);
    } on SleepTrackerPlatformException catch (e) {
      setState(() {
        _status = _Status.error;
        _errorDetail = '${e.code}${e.message != null ? ': ${e.message}' : ''}';
      });
    } finally {
      setState(() => _busy = false);
      await _refresh();
    }
  }

  Future<void> _stop() async {
    setState(() => _busy = true);
    try {
      await _tracker.stopTracking();
    } finally {
      setState(() => _busy = false);
      await _refresh();
    }
  }

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return Scaffold(
      backgroundColor: scheme.surfaceContainerLowest,
      appBar: AppBar(title: const Text('android_sleep_tracker example')),
      body: RefreshIndicator(
        onRefresh: _refresh,
        child: ListView(
          padding: const EdgeInsets.all(20),
          children: [
            _TrackingStatusCard(isTracking: _isTracking),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: FilledButton.icon(
                    onPressed: _busy || _isTracking ? null : _start,
                    icon: const Icon(Icons.bedtime_outlined),
                    label: const Text('Start tracking'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: _busy || !_isTracking ? null : _stop,
                    icon: const Icon(Icons.stop_circle_outlined),
                    label: const Text('Stop tracking'),
                  ),
                ),
              ],
            ),
            _buildStatusBanner(),
            const SizedBox(height: 28),
            Row(
              children: [
                Text(
                  'Live events',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                const SizedBox(width: 8),
                if (_liveEvents.isNotEmpty)
                  Text(
                    '(${_liveEvents.length})',
                    style: Theme.of(context).textTheme.bodySmall
                        ?.copyWith(color: scheme.outline),
                  ),
              ],
            ),
            const SizedBox(height: 12),
            _liveEvents.isEmpty
                ? const _EmptyStateCard(
                    icon: Icons.sensors_outlined,
                    message: 'Waiting for live Sleep API events…',
                  )
                : Column(
                    children: _liveEvents
                        .map((event) => _LiveEventTile(event: event))
                        .toList(),
                  ),
          ],
        ),
      ),
    );
  }

  Widget _buildStatusBanner() {
    final (icon, message, color) = switch (_status) {
      _Status.idle => (null, null, null),
      _Status.unsupported => (
        Icons.error_outline,
        'Sleep tracking is unsupported on this device '
            '(missing/outdated Google Play Services, or non-Android host).',
        Colors.orange,
      ),
      _Status.permissionDenied => (
        Icons.block,
        'ACTIVITY_RECOGNITION permission was denied. '
            'Grant it in system settings to start tracking.',
        Colors.red,
      ),
      _Status.error => (
        Icons.error_outline,
        'Platform error: $_errorDetail',
        Colors.red,
      ),
    };
    if (message == null) return const SizedBox.shrink();
    return Padding(
      padding: const EdgeInsets.only(top: 12),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(icon, size: 18, color: color),
          const SizedBox(width: 8),
          Expanded(
            child: Text(message, style: TextStyle(color: color)),
          ),
        ],
      ),
    );
  }
}

class _TrackingStatusCard extends StatelessWidget {
  const _TrackingStatusCard({required this.isTracking});

  final bool isTracking;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    final color = isTracking ? scheme.primary : scheme.outline;
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
      decoration: BoxDecoration(
        color: isTracking
            ? scheme.primaryContainer
            : scheme.surfaceContainerHigh,
        borderRadius: BorderRadius.circular(16),
      ),
      child: Row(
        children: [
          Icon(
            isTracking ? Icons.bedtime : Icons.bedtime_off_outlined,
            color: color,
          ),
          const SizedBox(width: 12),
          Text(
            isTracking ? 'Tracking sleep' : 'Not tracking',
            style: Theme.of(context).textTheme.titleMedium
                ?.copyWith(fontWeight: FontWeight.w600, color: color),
          ),
          const Spacer(),
          _StatusDot(active: isTracking, color: color),
        ],
      ),
    );
  }
}

class _StatusDot extends StatelessWidget {
  const _StatusDot({required this.active, required this.color});

  final bool active;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 10,
      height: 10,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: active ? color : Colors.transparent,
        border: Border.all(color: color, width: 1.5),
      ),
    );
  }
}

/// Shared surface for every card below — same fill color and rounded
/// corners, just padding/radius vary.
class _SurfaceCard extends StatelessWidget {
  const _SurfaceCard({
    required this.child,
    this.padding = const EdgeInsets.all(18),
    this.borderRadius = 16,
  });

  final Widget child;
  final EdgeInsetsGeometry padding;
  final double borderRadius;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: padding,
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.surfaceContainerHigh,
        borderRadius: BorderRadius.circular(borderRadius),
      ),
      child: child,
    );
  }
}

/// Centered icon + message placeholder for the "no live events yet" state.
class _EmptyStateCard extends StatelessWidget {
  const _EmptyStateCard({required this.icon, required this.message});

  final IconData icon;
  final String message;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    return _SurfaceCard(
      padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 16),
      child: Column(
        children: [
          Icon(icon, size: 28, color: scheme.outline),
          const SizedBox(height: 10),
          Text(
            message,
            style: Theme.of(context).textTheme.bodyMedium
                ?.copyWith(color: scheme.outline),
          ),
        ],
      ),
    );
  }
}

class _LiveEventTile extends StatelessWidget {
  const _LiveEventTile({required this.event});

  final SleepApiEvent event;

  @override
  Widget build(BuildContext context) {
    final scheme = Theme.of(context).colorScheme;
    // Bind into a local so the switch patterns below can promote it — Dart
    // doesn't promote a field/property access (`this.event`), only locals.
    final e = event;
    final (icon, title, subtitle) = switch (e) {
      SleepSegmentReceived() => (
        Icons.bedtime_outlined,
        'Segment · ${e.status.name}',
        e.status == SleepSegmentStatus.notDetected
            ? 'No sleep detected in the past day'
            : '${_formatDateTime(e.startTime)} → ${_formatDateTime(e.endTime)}',
      ),
      SleepClassifyReceived() => (
        Icons.insights_outlined,
        'Classify · confidence ${e.confidence}',
        '${_formatDateTime(e.timestamp)} · motion ${e.motion} · light ${e.light}',
      ),
    };
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: _SurfaceCard(
        padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
        borderRadius: 12,
        child: Row(
          children: [
            Icon(icon, size: 18, color: scheme.primary),
            const SizedBox(width: 10),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(title, style: Theme.of(context).textTheme.bodyMedium),
                  Text(
                    subtitle,
                    style: Theme.of(context).textTheme.bodySmall
                        ?.copyWith(color: scheme.outline),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

final _dateTimeFormat = DateFormat('MMM d, h:mm a');

String _formatDateTime(DateTime dt) => _dateTimeFormat.format(dt);
1
likes
160
points
68
downloads

Documentation

API reference

Publisher

verified publisherdashstack.tech

Weekly Downloads

Flutter API over Android's native sleep tracking (Google Play Services Activity Recognition Sleep API). Android-only, no UI, no health scoring.

Repository (GitHub)
View/report issues

Topics

#android #sleep #health #sensors

License

MIT (license)

Dependencies

flutter

More

Packages that depend on android_sleep_tracker

Packages that implement android_sleep_tracker