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 rawPlatformException. - 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: < 50 → SleepStageType.awake, >= 50 →
SleepStageType.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+SharedPreferenceson 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.