flutter_device_compass

pub package Platform License

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 uses SensorManager.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.hasSensors before mounting compass views.
  • ⚡ Robust Sensor Fallbacks: Uses Sensor.TYPE_ROTATION_VECTOR on Android when available with low-pass filtered accelerometer + magnetometer fallback.
  • 🔋 Tunable updates: CompassUpdateOptions presets (fine 0.1° default, balanced 0.5°, batterySaver 1.0°) control heading filter, motion rate, and emit interval.
  • 🛡️ Crash-Resistant: Guarded against non-finite / NaN sensor 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_handler to 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();
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); updateIntervalMs caps Flutter events.
  • Android: headingFilter is a minimum heading delta between emissions; updateIntervalMs sets emit throttle and sensor delay hint; motionUpdateHz is 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!