# Datasource Listeners

Last updated:

<!-- -->

October 18, 2026

<!-- -->

\| 5 minutes read

<br />

<br />

This example demonstrates how to register a listener **all GemKit sensors** on a live `DataSource`. The UI provides buttons to trigger permission requests and attach the listeners.

## How it works[​](#how-it-works "Direct link to How it works")

* Requests **location**, **camera**, and other **sensors** permissions with `permission_handler` package.
* Creates a live `DataSource` and registers a **single** `DataSourceListener` for every `DataType`.
* Safely casts each incoming `SenseData` and prints structured logs.

![Initial map screen](/docs/flutter/assets/images/example_flutter_datasource_listeners1-e5b79eede96ddd0dd479ce54451f71f6.png)

**Initial map screen**

![Device Sensors Data Page](/docs/flutter/assets/images/example_flutter_datasource_listeners2-72bb9c9afe074217cd992c28b330d52a.png)

**Device Sensors Data Page**

### Main App[​](#main-app "Direct link to Main App")

main.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/data_source_listeners/lib/main.dart?ref_type=heads#L6)

```dart
import 'package:flutter/material.dart' hide Animation, Route, Orientation;

import 'package:magiclane_maps_flutter/magiclane_maps_flutter.dart';
import 'package:datasource_listeners/device_sensors_data.dart';
import 'package:permission_handler/permission_handler.dart';

const projectApiToken = String.fromEnvironment('YOUR_API_TOKEN_HERE');
void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Datasource Listeners',
      home: MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool hasGrantedPermissions = false;
  @override
  void dispose() {
    GemKit.release();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          "Datasource Listeners",
          style: TextStyle(color: Colors.white),
        ),
        backgroundColor: Colors.deepPurple[900],
        actions: [
          IconButton(
            onPressed: () async {
              await _requestPermissions();
              if (!context.mounted) return;
              Navigator.push(
                context,
                MaterialPageRoute<void>(
                  builder: (context) => const DeviceSensorsDataPage(),
                ),
              );
            },
            icon: const Icon(Icons.article_outlined, color: Colors.white),
          ),
        ],
      ),
      body: GemMap(key: ValueKey("GemMap"), appAuthorization: projectApiToken),
      resizeToAvoidBottomInset: false,
    );
  }

  Future<void> _requestPermissions() async {
    final permissions = [
      Permission.location,
      Permission.locationAlways,
      Permission.locationWhenInUse,
      Permission.sensors,
      Permission.camera,
    ];
    for (final permission in permissions) {
      final status = await permission.status;
      if (status.isDenied || status.isPermanentlyDenied) {
        await permission.request();
      }
    }
  }
}

```

### Device Sensors Data Page[​](#device-sensors-data-page "Direct link to Device Sensors Data Page")

device\_sensors\_data.dart[View on Github](https://github.com/magiclane/magiclane-maps-sdk-examples-for-flutter/tree/main/data_source_listeners/lib/device_sensors_data.dart?ref_type=heads#L6)

```dart
import 'package:flutter/material.dart';
import 'package:magiclane_maps_flutter/sense.dart' as sense;
import 'package:magiclane_maps_flutter/position.dart' as position;

/// Device sensors UI
class DeviceSensorsDataPage extends StatefulWidget {
  const DeviceSensorsDataPage({super.key});

  @override
  State<DeviceSensorsDataPage> createState() => _DeviceSensorsDataPageState();
}

class _DeviceSensorsDataPageState extends State<DeviceSensorsDataPage> {
  sense.DataType? _selectedType;
  final Map<sense.DataType, sense.SenseData> _latest = {};
  sense.DataSource? _dataSource;
  sense.DataSourceListener? _listener;

  @override
  void initState() {
    super.initState();
    _initDataSourceAndListeners();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        foregroundColor: Colors.white,
        backgroundColor: Colors.deepPurple[900],
        title: const Text(
          'Device Sensors Data',
          style: TextStyle(color: Colors.white),
        ),
        elevation: 0,
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          _buildButtonsRow(),
          const Divider(height: 1),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(12.0),
              child: SingleChildScrollView(
                child: Text(
                  _displayFor(_selectedType),
                  style: const TextStyle(fontSize: 16),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _initDataSourceAndListeners() async {
    _dataSource = sense.DataSource.createLiveDataSource();
    if (_dataSource == null) {
      // Live data not available.
      return;
    }

    _listener = sense.DataSourceListener(
      onNewData: (sense.SenseData data) {
        if (!mounted) return;
        setState(() {
          _latest[data.type] = data;
        });
      },
    );

    for (final t in sense.DataType.values) {
      if (t == sense.DataType.unknown) continue;
      _dataSource!.addListener(listener: _listener!, dataType: t);
    }

    _dataSource!.start();
  }

  @override
  void dispose() {
    try {
      if (_listener != null) {
        for (final t in sense.DataType.values) {
          if (t == sense.DataType.unknown) continue;
          _dataSource?.removeListener(listener: _listener!, dataType: t);
        }
      }
      _listener?.dispose();
      _dataSource?.dispose();
    } catch (_) {
      // ignore errors during cleanup
    }
    super.dispose();
  }

  Widget _buildButtonsRow() {
    final types = sense.DataType.values
        .where(
          (t) => t != sense.DataType.unknown && t != sense.DataType.gyroscope,
        )
        .toList();
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      padding: const EdgeInsets.all(8),
      child: Row(
        children: types.map((t) {
          final selected = t == _selectedType;
          return Padding(
            padding: const EdgeInsets.symmetric(horizontal: 6),
            child: ChoiceChip(
              label: Text(t.toString().split('.').last),
              selected: selected,
              onSelected: (_) => setState(() => _selectedType = t),
            ),
          );
        }).toList(),
      ),
    );
  }

  String _displayFor(sense.DataType? type) {
    if (type == null) {
      return 'No type selected.';
    }
    final data = _latest[type];
    if (data == null) {
      return 'No data received yet for ${type.toString().split('.').last}.';
    }

    switch (type) {
      case sense.DataType.acceleration:
        final d = data as sense.Acceleration;
        return 'Acceleration x=${d.x} y=${d.y} z=${d.z} ${d.unit}';
      case sense.DataType.attitude:
        final d = data as sense.Attitude;
        return 'Attitude roll=${d.roll} pitch=${d.pitch} yaw=${d.yaw}';
      case sense.DataType.battery:
        final d = data as sense.Battery;
        return 'Battery level=${d.level}% state=${d.state}';
      case sense.DataType.camera:
        final d = data as sense.Camera;
        final cfg = d.cameraConfiguration;
        return 'Camera ${cfg.frameWidth}x${cfg.frameHeight}@${cfg.frameRate}fps';
      case sense.DataType.compass:
        final d = data as sense.Compass;
        return 'Heading=${d.heading}° acc=${d.accuracy}';
      case sense.DataType.magneticField:
        final d = data as sense.MagneticField;
        return 'Mag x=${d.x} y=${d.y} z=${d.z} µT';
      case sense.DataType.orientation:
        final d = data as sense.Orientation;
        return 'Orientation=${d.orientation} face=${d.face}';
      case sense.DataType.position:
        final d = data as position.GemPosition;
        return 'Pos ${d.latitude.toStringAsFixed(6)}, ${d.longitude.toStringAsFixed(6)} alt=${d.altitude}m';
      case sense.DataType.improvedPosition:
        final d = data as position.GemImprovedPosition;
        final roadMods = d.roadModifiers.isEmpty
            ? 'none'
            : d.roadModifiers
                  .map((m) => m.toString().split('.').last)
                  .join(',');
        final addr = () {
          try {
            return d.address.format();
          } catch (_) {
            return '';
          }
        }();

        final sb = StringBuffer();
        sb.writeln(
          'Position: ${d.latitude.toStringAsFixed(6)}, ${d.longitude.toStringAsFixed(6)}',
        );
        sb.writeln('Altitude: ${d.altitude} m');
        sb.writeln('Provider: ${d.provider.toString().split('.').last}');

        final speedLine = StringBuffer(
          'Speed: ${d.speed.toStringAsFixed(2)} m/s',
        );
        if (d.hasSpeedAccuracy) {
          speedLine.write(' ±${d.speedAccuracy.toStringAsFixed(2)} m/s');
        }
        sb.writeln(speedLine.toString());

        final courseLine = StringBuffer(
          'Course: ${d.course.toStringAsFixed(1)}°',
        );
        if (d.hasCourseAccuracy) {
          courseLine.write(' ±${d.courseAccuracy.toStringAsFixed(1)}°');
        }
        sb.writeln(courseLine.toString());
        sb.writeln('Fix quality: ${d.fixQuality.toString().split('.').last}');

        sb.writeln('Horizontal accuracy: ${d.accuracyH.toStringAsFixed(1)} m');
        sb.writeln('Vertical accuracy: ${d.accuracyV.toStringAsFixed(1)} m');

        // Improved-position specific fields
        sb.writeln('Road modifiers: $roadMods');
        sb.writeln('Speed limit: ${d.speedLimit.toStringAsFixed(2)} m/s');
        sb.writeln('Road localization: ${d.hasRoadLocalization}');
        sb.writeln('Terrain data available: ${d.hasTerrainData}');

        sb.writeln(
          'Terrain altitude: ${d.terrainAltitude.toStringAsFixed(1)} m',
        );
        sb.writeln('Terrain slope: ${d.terrainSlope.toStringAsFixed(1)}°');

        if (addr.isNotEmpty) {
          sb.writeln('Address: $addr');
        }

        return sb.toString().trim();
      case sense.DataType.rotationRate || sense.DataType.gyroscope:
        final d = data as sense.RotationRate;
        return 'RotationRate x=${d.x} y=${d.y} z=${d.z}';
      case sense.DataType.temperature:
        final d = data as sense.Temperature;
        return 'Temp=${d.temperature}°C level=${d.level}';
      case sense.DataType.notification:
        return 'Notification at ${data.acquisitionTime.toUtc()}';
      case sense.DataType.mountInformation:
        final d = data as sense.MountInformation;
        return 'Mounted=${d.isMountedForCameraUse} portrait=${d.isPortraitMode}';
      case sense.DataType.heartRate:
        final d = data as sense.HeartRate;
        return 'HR=${d.heartRate} bpm';
      case sense.DataType.nmeaChunk:
        final d = data as sense.NmeaChunk;
        return 'NMEA: ${d.nmeaChunk}';
      default:
        return 'Unknown';
    }
  }
}

```

### Permissions[​](#permissions "Direct link to Permissions")

This sample requests Location (When In Use + Always), Camera, and Body Sensors.

Required Permissions

To ensure this example functions correctly, the necessary permissions must be added to the project's Android and iOS configuration files:

* Android
* iOS

Add the following code to the `android/app/src/main/AndroidManifest.xml` file, within the `<manifest>` block:

```
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.BODY_SENSORS" />
<uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND" />
```

This example uses the<!-- --> [Permission Handler](https://pub.dev/packages/permission_handler) <!-- -->package. Be sure to follow the<!-- --> [setup guide](https://pub.dev/packages/permission_handler#setup).

Add the following to `ios/Runner/Info.plist` inside the `<dict>`:

```
<key>NSLocationWhenInUseUsageDescription</key>
<string>Location is needed for map localization and navigation</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Location is needed for map localization and navigation</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
<key>NSCameraUsageDescription</key>
<string>Camera access is needed for video recording</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access to record audio.</string>
<key>NSMotionUsageDescription</key>
<string>We use motion sensors to read acceleration, rotation, and orientation.</string>
```

This example uses the<!-- --> [Permission Handler](https://pub.dev/packages/permission_handler) <!-- -->package. Follow the<!-- --> [official setup instructions](https://pub.dev/packages/permission_handler#setup). Add this to your `ios/Podfile`:

```
post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)

    target.build_configurations.each do |config|
      config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
        '$(inherited)',
        'PERMISSION_LOCATION=1',
        'PERMISSION_CAMERA=1',
	'PERMISSION_MICROPHONE=1'
      ]
    end
  end
end
```
