dashstack_poster

android_sleep_tracker

Flutter API over Android's native sleep tracking (Google Play Services' Activity Recognition — Sleep API) — one simple Dart API, no platform-channel code to write. Android-only. No iOS, no health scoring, no bundled UI, no Firebase.

Requires Flutter >= 3.3.0, Dart >= 3.13.0, Android minSdkVersion >= 24.

First time using this? Sleep tracking won't detect anything until you add a permission to your own app's manifest. Read the "Setup" section below before you test — this is the #1 source of "no data ever shows up".

Install

dependencies:
  android_sleep_tracker: <latest_version>
flutter pub get

Quick start

import 'package:android_sleep_tracker/android_sleep_tracker.dart';

final tracker = AndroidSleepTracker.instance;

await tracker.startTracking();

final latest = await tracker.getLatestSleep();
if (latest != null) {
  print('slept ${latest.duration} from ${latest.startTime} to ${latest.endTime}');
}

Sleep/awake segments are detected by the OS in the background — there's nothing to poll. Call startTracking() once; the plugin's manifest-merged BroadcastReceiver keeps recording even if your app is killed.

All methods

Call these as AndroidSleepTracker.instance.<method>:

Method Returns Notes
startTracking() Future<void> Requests the Sleep API + ACTIVITY_RECOGNITION permission if needed
stopTracking() Future<void>
isTracking() Future<bool> Read-only, cheap, no live device call
getLatestSleep() Future<SleepSession?> Most recent recorded session, or null
clearHistory() Future<void> Wipes stored sessions/classify samples. Doesn't stop tracking — new data keeps appending if still on
sleepDataStream Stream<SleepApiEvent> Live, push-based — see below

Full working app: example/lib/main.dart.

Live event stream

sleepDataStream pushes every raw SleepSegmentEvent/SleepClassifyEvent the moment the OS delivers it — no polling getLatestSleep() in a timer. Requires startTracking() to have been called first (the stream just relays what's registered; it doesn't itself start tracking):

final subscription = AndroidSleepTracker.instance.sleepDataStream.listen(
  (event) => switch (event) {
    SleepSegmentReceived() => print('segment: $event'),
    SleepClassifyReceived() => print('classify sample: $event'),
  },
  onError: (Object error) => print('sleep stream error: $error'),
);

// later
await subscription.cancel();
  • It's a broadcast stream — listen/cancel as many times as you like, from multiple places, without stepping on other listeners.
  • Errors surface the same typed exceptions as the method calls (SleepTrackerUnsupportedException, etc.), never a raw PlatformException.
  • Lifecycle limitation: only delivers events while a subscription is active and the app/engine is alive. Anything the OS delivers while the app is killed is still durably persisted natively and catchable afterward via getLatestSleep() — it just isn't streamed live after the fact.

Models

class SleepSession {
  final DateTime startTime;
  final DateTime endTime;
  final Duration duration;
  final List<SleepStage>? sleepStages; // null/empty on most devices, see limitation below
}

enum SleepStageType { awake, light, deep, rem, unknown }

class SleepStage {
  final DateTime start;
  final DateTime end;
  final SleepStageType type;
}

sealed class SleepApiEvent {}

class SleepSegmentReceived extends SleepApiEvent {
  final SleepSegmentStatus status; // successful, missingData, notDetected, unknown
  final DateTime startTime;
  final DateTime endTime;
  final Duration duration;
}

class SleepClassifyReceived extends SleepApiEvent {
  final DateTime timestamp;
  final int confidence; // 0-100
  final int motion;
  final int light;
}

Sleep-stage limitation — Android exposes no public API for real multi-stage (light/deep/REM) sleep detection without a third-party wearable SDK. This package never synthesizes stage data — SleepSession.sleepStages is a coarse, best-effort guess built from SleepClassifyEvent's 0-100 sleep-confidence score: < 50SleepStageType.awake, >= 50SleepStageType.light. deep/rem are never produced, and light here means only "the model was reasonably confident the user was asleep" — not a real light-sleep-stage read. sleepStages is null if no classify samples landed within a session's window.

⚙️ Setup (required)

Add these to your app, not this plugin — it can't do it for you.

1. android/app/src/main/AndroidManifest.xml — the runtime permission:

<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />

Missing it means startTracking() throws SleepTrackerPermissionDeniedException immediately, before the OS even shows a dialog.

2. Nothing else. The plugin's own manifest merges its BroadcastReceiver declaration into your app automatically — startTracking() handles the runtime permission request/result for you.

Permission & unsupported-device handling

Condition Result
Play Services missing / host not Android SleepTrackerUnsupportedException
ACTIVITY_RECOGNITION denied by user SleepTrackerPermissionDeniedException from startTracking()
Permission denied, isTracking()/getLatestSleep() called Returns false/null quietly — read-only calls never throw for permission reasons
Native call fails for another reason SleepTrackerPlatformException(code, message)

Callers never see a raw PlatformException — everything is mapped to one of the typed exceptions above.

Troubleshooting

getLatestSleep()/sleepDataStream never produce anything — the Sleep API isn't instant. It classifies sleep only after a full still-period pattern completes, and often delivers the result well after you wake (sometimes hours later, OS-scheduled, not app-controlled). Short test sessions frequently produce nothing. Also check: real device (the Sleep API needs real accelerometer/light sensors — most emulators produce none of this data), isTracking() actually true, and Google Play Services is up to date.

SleepTrackerUnsupportedException always thrown — device has no/outdated Google Play Services, or you're running on a non-Android host.

A session's startTime looks earlier than when you pressed "start" — the OS subscription (requestSleepSegmentUpdates) stays registered across app restarts until stopTracking() explicitly removes it. If an earlier run never called stopTracking(), Play Services kept monitoring the whole time, and the next segment reflects that full window, not just your latest "start" tap.

Data stopped arriving after force-stopping the app — Android blocks all broadcasts (including this plugin's manifest receiver) to a force-stopped app until the user reopens it by hand. This is an OS restriction; swiping the app from recents is not the same as force-stop and doesn't trigger it.

Notes

  • No polling, no foreground service — the Sleep API is push/broadcast based and OS-scheduled.
  • No Room/DB, no Gson/Moshi — plain org.json + SharedPreferences on the native side.
  • No sleep score/efficiency/health calculations — raw segments/sessions only, no fine-grained sleep-stage sensor fusion.
  • No Firebase, no analytics, no cloud sync.

Bugs & Credits

Report bugs and ask questions on GitHub Issues. Maintained by Dashstack Infotech, Surat.

Libraries

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