flutter_device_compass
A Flutter compass plugin using device sensors. Provides real-time device heading, orientation-compensated azimuth (0°–360°, where 0° is North), camera-mode heading for AR/camera overlays, and sensor accuracy.
Community-maintained fork of medyas/flutter_compass_v2 (lineage from hemanthrajv/flutter_compass), republished as flutter_device_compass on pub.dev for ongoing maintenance and modern Flutter (3.24+ / 3.44+) & Gradle/Kotlin compatibility.
Features
- 🧭 Heading Calculation: 0° to 360°, where 0° is North.
- 📱 Orientation-Aware: Automatically compensates for device orientation changes (Portrait, Landscape Left, Landscape Right, Portrait Upside Down).
- 📷 Camera Mode Heading: Heading out the back of the device (
headingForCameraMode), ideal for Augmented Reality (AR) or camera overlays. On Android this usesSensorManager.remapCoordinateSystem(..., AXIS_X, AXIS_Z, ...)per platform docs. - 🎯 Sensor Accuracy: Exposes calibration and accuracy deviation in degrees (
accuracy). - 🔍 Hardware Detection: Proactively check sensor availability via
FlutterCompass.hasSensorsbefore mounting compass views. - ⚡ Robust Sensor Fallbacks: Uses
Sensor.TYPE_ROTATION_VECTORon Android when available with low-pass filtered accelerometer + magnetometer fallback. - 🔋 Tunable updates:
CompassUpdateOptionspresets (fine0.1° default,balanced0.5°,batterySaver1.0°) control heading filter, motion rate, and emit interval. - 🛡️ Crash-Resistant: Guarded against non-finite /
NaNsensor readings and negative true headings.
Getting Started
Add flutter_device_compass to your pubspec.yaml:
dependencies:
flutter_device_compass: ^2.2.0
Platform Setup
iOS
iOS requires location permissions to deliver compass/heading updates via CoreLocation.
Add the following keys to your ios/Runner/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to heading and compass data to orient the map.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs access to heading and compass data to orient the map.</string>
Note: Use a permission package like
permission_handlerto request location permissions at runtime before listening to compass events on iOS.
Android
No additional permissions are strictly required for sensor access. However, if your app utilizes fine location for geomagnetic declination or maps alongside the compass, add the following to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Usage Examples
Backward compatibility (no breaking change)
Existing apps keep working with zero code changes:
// Legacy API — still valid. EventChannel listen args are null;
// native plugins apply built-in balanced defaults.
FlutterCompass.events?.listen((event) { /* ... */ });
New update tuning is opt-in via nullable [CompassUpdateOptions?]:
| Call | Platform listen arguments | Behavior |
|---|---|---|
FlutterCompass.events |
null |
Same as ≤2.1.1; native fine (0.1°) defaults |
FlutterCompass.eventsFor() |
null |
Same as events |
FlutterCompass.eventsFor(CompassUpdateOptions.balanced) |
{ headingFilter, motionUpdateHz, updateIntervalMs } |
Custom profile |
FlutterCompass.eventsFor(null) |
null |
Explicit legacy path |
Native code treats each map field as optional: missing/null keys keep platform defaults. Old app binaries talking to a new plugin (or the reverse during upgrades) therefore stay safe as long as they use FlutterCompass.events.
1. Basic Stream Subscription
import 'package:flutter_device_compass/flutter_device_compass.dart';
// Default — unchanged from older versions:
final subscription = FlutterCompass.events?.listen((CompassEvent event) {
print('Heading: ${event.heading}°');
print('Camera Mode Heading: ${event.headingForCameraMode}°');
print('Accuracy Deviation: ±${event.accuracy}°');
});
// Optional new API (additive):
FlutterCompass.eventsFor(CompassUpdateOptions.balanced)?.listen((event) { /* ... */ });
FlutterCompass.eventsFor(CompassUpdateOptions.batterySaver)?.listen((event) { /* ... */ });
FlutterCompass.eventsFor(const CompassUpdateOptions(
headingFilter: 0.1,
motionUpdateHz: 30,
updateIntervalMs: 33,
))?.listen((event) { /* ... */ });
// Cancel when no longer needed
// subscription?.cancel();
// Before switching profiles: cancel listeners, then FlutterCompass.resetEvents();
Choosing headingFilter (and related knobs)
| Preset | headingFilter |
Typical feel | Battery | Defaults applied |
|---|---|---|---|---|
CompassUpdateOptions.fine (default) |
0.1° | Very smooth / chatty needle; historical default | Higher | motion 30 Hz, emit ≥33 ms |
CompassUpdateOptions.balanced |
0.5° | Smooth enough for most UIs | Medium | motion 20 Hz, emit ≥50 ms |
CompassUpdateOptions.batterySaver |
1.0° | Slightly steppier; fewer callbacks | Lower | motion 15 Hz, emit ≥66 ms |
- iOS:
headingFilter→CLLocationManager.headingFilter;motionUpdateHz→ CoreMotion sample rate (camera heading);updateIntervalMscaps Flutter events. - Android:
headingFilteris a minimum heading delta between emissions;updateIntervalMssets emit throttle and sensor delay hint;motionUpdateHzis ignored. - When switching options at runtime, cancel the previous subscription (or call
FlutterCompass.resetEvents()after cancel) so the EventChannel can restart with new args.
2. Building a Rotating Compass UI (StreamBuilder)
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_device_compass/flutter_device_compass.dart';
class CompassWidget extends StatelessWidget {
const CompassWidget({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<CompassEvent>(
stream: FlutterCompass.events,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final heading = snapshot.data?.heading;
// On devices without compass sensors, heading will be null
if (heading == null) {
return const Center(
child: Text('Device does not have compass sensors'),
);
}
return Center(
child: Transform.rotate(
angle: (heading * (math.pi / 180) * -1),
child: Image.asset(
'assets/compass.png',
width: 250,
height: 250,
),
),
);
},
);
}
}
3. Check Sensor Availability
Before initiating compass-dependent features, you can verify if the device hardware supports compass sensors:
final bool? isSupported = await FlutterCompass.hasSensors;
if (isSupported == true) {
print('Compass sensor is available on this device');
} else {
print('Device does not support compass/heading sensors');
}
4. Read Current Heading Once
If you only need a single heading snapshot (e.g. when capturing a photo or logging location):
Future<void> logCurrentHeading() async {
final CompassEvent event = await FlutterCompass.events!.first;
print('Current heading is: ${event.heading}°');
}
CompassEvent Reference
| Property | Type | Description |
|---|---|---|
heading |
double? |
The device heading in degrees around its Z-axis (pointing direction of the top of the phone). Range: 0.0 to 360.0, where 0.0 is North. null if sensor is unavailable. |
headingForCameraMode |
double? |
The heading in degrees out the back of the phone (rear camera / AR). Ideal for camera overlays. |
accuracy |
double? |
Estimated deviation error in degrees (±). null if uncalibrated or unavailable. |
Releases
GitHub Releases / tags track pub.dev versions. Check Releases for changelogs per version.
Maintainer
Maintained with ❤️ by Nafis Kabbo.
Contributions, issues, and feature requests are welcome on GitHub!