Reading Device Sensors
Reading Device Sensors in Flutter
Modern smartphones contain a rich array of hardware sensors that measure physical forces, orientation, and motion. Flutter provides access to these sensors through the sensors_plus package, which wraps the native iOS (Core Motion) and Android (SensorManager) APIs into a clean, stream-based Dart interface. In this lesson you will learn how to subscribe to the accelerometer and gyroscope streams, interpret the three-axis output, and build a live sensor dashboard that reacts in real time to device movement.
Adding the Dependency
Add sensors_plus to your pubspec.yaml. No additional platform permissions are required on either iOS or Android for reading the accelerometer and gyroscope — they are considered non-sensitive sensors.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
sensors_plus: ^4.0.2 # always check pub.dev for the latest stable version
Run flutter pub get after saving the file.
Understanding the Sensor Streams
The sensors_plus package exposes each sensor as a Dart Stream. The two most commonly used streams are:
- accelerometerEventStream() — emits
AccelerometerEventobjects containing x, y, and z values measured in m/s². These values include the force of gravity (~9.81 m/s² at rest on a flat surface). - gyroscopeEventStream() — emits
GyroscopeEventobjects containing the rate of rotation around each axis in rad/s. Values near zero mean the device is stationary. - userAccelerometerEventStream() — same as accelerometerEventStream but with gravity subtracted, so a still device reports (0, 0, 0).
dispose() to prevent memory leaks and battery drain after the widget is removed from the tree.Axis Conventions
Both sensors follow the same coordinate system as the Android/iOS SDK. When the device lies flat on a table with the screen facing up:
- X axis — points to the right of the device (landscape right = positive)
- Y axis — points toward the top of the device (upward = positive)
- Z axis — points out of the screen face (away from you = positive)
userAccelerometerEventStream() instead of accelerometerEventStream() when you want to detect only user-initiated movement and ignore gravity. This is what fitness apps use to count steps.Subscribing to Sensor Streams
The cleanest pattern is to store each StreamSubscription as a field in your State class, start them in initState(), and cancel them in dispose(). Inside the event callback, call setState() to trigger a UI rebuild.
Live Sensor Dashboard Widget
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sensors_plus/sensors_plus.dart';
class SensorDashboard extends StatefulWidget {
const SensorDashboard({super.key});
@override
State<SensorDashboard> createState() => _SensorDashboardState();
}
class _SensorDashboardState extends State<SensorDashboard> {
// Latest accelerometer reading (m/s²)
double _accelX = 0, _accelY = 0, _accelZ = 0;
// Latest gyroscope reading (rad/s)
double _gyroX = 0, _gyroY = 0, _gyroZ = 0;
late final StreamSubscription<AccelerometerEvent> _accelSub;
late final StreamSubscription<GyroscopeEvent> _gyroSub;
@override
void initState() {
super.initState();
// Subscribe to accelerometer at 20 Hz (Normal UI update rate)
_accelSub = accelerometerEventStream(
samplingPeriod: SensorInterval.normalInterval,
).listen((AccelerometerEvent event) {
setState(() {
_accelX = event.x;
_accelY = event.y;
_accelZ = event.z;
});
});
// Subscribe to gyroscope at the same rate
_gyroSub = gyroscopeEventStream(
samplingPeriod: SensorInterval.normalInterval,
).listen((GyroscopeEvent event) {
setState(() {
_gyroX = event.x;
_gyroY = event.y;
_gyroZ = event.z;
});
});
}
@override
void dispose() {
// Always cancel subscriptions to avoid memory leaks
_accelSub.cancel();
_gyroSub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sensor Dashboard')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SensorCard(
title: 'Accelerometer (m/s²)',
x: _accelX, y: _accelY, z: _accelZ,
),
const SizedBox(height: 16),
_SensorCard(
title: 'Gyroscope (rad/s)',
x: _gyroX, y: _gyroY, z: _gyroZ,
),
],
),
),
);
}
}
// Reusable card for displaying three-axis sensor data
class _SensorCard extends StatelessWidget {
final String title;
final double x, y, z;
const _SensorCard({
required this.title,
required this.x,
required this.y,
required this.z,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text('X: ${x.toStringAsFixed(3)}'),
Text('Y: ${y.toStringAsFixed(3)}'),
Text('Z: ${z.toStringAsFixed(3)}'),
],
),
),
);
}
}
Controlling the Sampling Rate
The samplingPeriod parameter controls how often the sensor fires an event. The package provides four convenience constants via SensorInterval:
SensorInterval.normalInterval— ~20 Hz, suitable for most UI updatesSensorInterval.uiInterval— matches the screen refresh rate (~60 Hz)SensorInterval.gameInterval— ~100 Hz, for games needing fast responseSensorInterval.fastestInterval— maximum hardware rate (device-dependent)
setState() calls. Use normalInterval for informational displays and only increase the rate when the use case genuinely requires it (e.g., a real-time game controller).Smoothing Noisy Readings with a Low-Pass Filter
Raw sensor data is inherently noisy. A simple low-pass filter blends the new reading with the previous value to reduce jitter without adding significant latency. The alpha parameter (0–1) controls how much weight to give the new sample: 0.1 is very smooth (slow), 0.9 is very responsive (noisy).
Low-Pass Filter Applied to Accelerometer Data
const double _alpha = 0.15; // smoothing factor
double _smoothX = 0, _smoothY = 0, _smoothZ = 0;
void _onAccelEvent(AccelerometerEvent event) {
setState(() {
// low-pass filter: blend new value with running average
_smoothX = _alpha * event.x + (1 - _alpha) * _smoothX;
_smoothY = _alpha * event.y + (1 - _alpha) * _smoothY;
_smoothZ = _alpha * event.z + (1 - _alpha) * _smoothZ;
});
}
Summary
In this lesson you learned how to access device hardware sensors using the sensors_plus package. The key points to remember are:
- Each sensor is exposed as a continuous Dart
Streamof typed event objects containing x, y, z values. - Subscribe in
initState()and always cancel indispose()to prevent resource leaks. - Choose the appropriate
SensorInterval— prefernormalIntervalto balance responsiveness and battery life. - Apply a low-pass filter to reduce noise in raw sensor data when building smooth visual experiences.