Using Packages like sensors and accelerometer in Flutter

Flutter empowers developers to create cross-platform applications with beautiful UIs and smooth performance. One common requirement for modern apps is utilizing device sensors, particularly the accelerometer. In this comprehensive guide, we’ll explore how to use packages like sensors_plus and related tools to access and leverage accelerometer data in your Flutter applications.

Why Use Accelerometer Data in Flutter?

Accelerometer data provides valuable insights into the device’s motion and orientation. Some popular use cases include:

  • Motion-based Gaming: Implement tilt-based controls.
  • Step Tracking: Count steps using accelerometer data.
  • Shake Detection: Trigger actions on device shake (e.g., undo, refresh).
  • Screen Orientation Detection: Customize UI based on device orientation.
  • Crash Detection: Detect sudden changes in acceleration indicating a possible fall.

Setting up the Environment

Before diving into code, let’s ensure your Flutter environment is correctly set up.

Step 1: Create a New Flutter Project

flutter create accelerometer_app

Step 2: Add the sensors_plus Package

Open your pubspec.yaml file and add the following dependency:

dependencies:
  flutter:
    sdk: flutter
  sensors_plus: ^4.0.2

Run flutter pub get in the terminal to install the package.

Using the sensors_plus Package

The sensors_plus package simplifies access to various device sensors. Let’s see how to access accelerometer data using this package.

Example 1: Reading Accelerometer Events

This code snippet shows how to listen for accelerometer events and display the data in the UI.

import 'package:flutter/material.dart';
import 'package:sensors_plus/sensors_plus.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Accelerometer Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: AccelerometerPage(),
    );
  }
}

class AccelerometerPage extends StatefulWidget {
  @override
  _AccelerometerPageState createState() => _AccelerometerPageState();
}

class _AccelerometerPageState extends State {
  double? x, y, z;

  @override
  void initState() {
    super.initState();
    accelerometerEvents.listen((AccelerometerEvent event) {
      setState(() {
        x = event.x;
        y = event.y;
        z = event.z;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Accelerometer Data'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &ltWidget>[
            Text('X: ${x?.toStringAsFixed(2)}'),
            Text('Y: ${y?.toStringAsFixed(2)}'),
            Text('Z: ${z?.toStringAsFixed(2)}'),
          ],
        ),
      ),
    );
  }
}

Explanation:

  • We import necessary packages: flutter/material.dart and sensors_plus/sensors_plus.dart.
  • The AccelerometerPage is a StatefulWidget to allow updating the UI.
  • In the initState method, we listen to accelerometerEvents using .listen(). Whenever a new event occurs, the setState method is called to update the values of x, y, and z.
  • The build method constructs the UI, displaying the x, y, and z values using Text widgets.
  • The ?.toStringAsFixed(2) ensures null safety and formats the double values to two decimal places.

Example 2: Building a Simple Shake Detector

This example demonstrates how to detect a shake gesture using accelerometer data. By monitoring the acceleration magnitude, we can identify sudden movements.

import 'package:flutter/material.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shake Detector Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ShakeDetectorPage(),
    );
  }
}

class ShakeDetectorPage extends StatefulWidget {
  @override
  _ShakeDetectorPageState createState() => _ShakeDetectorPageState();
}

class _ShakeDetectorPageState extends State {
  double? x, y, z;
  DateTime? lastShakeTime;
  bool isShaking = false;

  @override
  void initState() {
    super.initState();
    accelerometerEvents.listen((AccelerometerEvent event) {
      setState(() {
        x = event.x;
        y = event.y;
        z = event.z;
      });

      // Calculate the acceleration magnitude
      double accelerationMagnitude = sqrt(x! * x! + y! * y! + z! * z!);

      // Threshold to detect shake (adjust this value)
      double shakeThreshold = 12.0;

      if (accelerationMagnitude > shakeThreshold) {
        DateTime now = DateTime.now();
        // If this is the first shake or the time since the last shake is long enough,
        // consider it a valid shake
        if (lastShakeTime == null || now.difference(lastShakeTime!) > Duration(milliseconds: 500)) {
          setState(() {
            isShaking = true;
          });

          // Reset the shake state after a short delay
          Future.delayed(Duration(milliseconds: 500), () {
            setState(() {
              isShaking = false;
            });
          });

          lastShakeTime = now; // Update the last shake time
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shake Detector'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &ltWidget>[
            Text(
              'Shake the device!',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            Text(
              isShaking ? 'Shaking!' : 'Not Shaking',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: isShaking ? Colors.red : Colors.green),
            ),
          ],
        ),
      ),
    );
  }
}

Explanation:

  • We listen to accelerometer events as before.
  • Inside the event listener, we calculate the acceleration magnitude using sqrt(x² + y² + z²).
  • A shakeThreshold variable determines the acceleration magnitude required to trigger a shake. You can adjust this value to fine-tune sensitivity.
  • A timestamp (lastShakeTime) is used to avoid multiple shake detections in rapid succession. A minimum time interval (500ms in this case) must pass between shakes for them to be considered valid.
  • If a shake is detected (accelerationMagnitude > shakeThreshold and the time condition is met), the isShaking boolean is set to true, triggering a UI update. The isShaking state is automatically reset after a short delay (500ms) for better UX.

Example 3: Combining with provider for state management

For more complex apps, managing the accelerometer data using state management solutions like provider is a good practice. Here’s an example:


import 'package:flutter/material.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:provider/provider.dart';
import 'dart:math';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => AccelerometerProvider(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Accelerometer Demo with Provider',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: AccelerometerPage(),
    );
  }
}

class AccelerometerProvider extends ChangeNotifier {
  double? x, y, z;
  DateTime? lastShakeTime;
  bool isShaking = false;

  AccelerometerProvider() {
    startListening();
  }

  void startListening() {
    accelerometerEvents.listen((AccelerometerEvent event) {
      updateAccelerometerData(event.x, event.y, event.z);
    });
  }

  void updateAccelerometerData(double newX, double newY, double newZ) {
    x = newX;
    y = newY;
    z = newZ;
    notifyListeners();
    detectShake();
  }

  void detectShake() {
    double accelerationMagnitude = sqrt(x! * x! + y! * y! + z! * z!);
    double shakeThreshold = 12.0;

    if (accelerationMagnitude > shakeThreshold) {
      DateTime now = DateTime.now();

      if (lastShakeTime == null || now.difference(lastShakeTime!) > Duration(milliseconds: 500)) {
        setIsShaking(true);

        Future.delayed(Duration(milliseconds: 500), () {
          setIsShaking(false);
        });

        lastShakeTime = now;
      }
    }
  }

  void setIsShaking(bool value) {
    isShaking = value;
    notifyListeners();
  }
}

class AccelerometerPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final accelerometerProvider = Provider.of&ltAccelerometerProvider>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Accelerometer Data'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: &ltWidget>[
            Text('X: ${accelerometerProvider.x?.toStringAsFixed(2)}'),
            Text('Y: ${accelerometerProvider.y?.toStringAsFixed(2)}'),
            Text('Z: ${accelerometerProvider.z?.toStringAsFixed(2)}'),
            SizedBox(height: 20),
            Text(
              accelerometerProvider.isShaking ? 'Shaking!' : 'Not Shaking',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: accelerometerProvider.isShaking ? Colors.red : Colors.green),
            ),
          ],
        ),
      ),
    );
  }
}

Key changes:

  • We wrap the MyApp widget with a ChangeNotifierProvider to make the AccelerometerProvider accessible throughout the app.
  • Accelerometer data is stored and managed within the AccelerometerProvider class, which extends ChangeNotifier.
  • The updateAccelerometerData and setIsShaking methods call notifyListeners() whenever the data changes. This triggers a UI rebuild to reflect the updated state.
  • The AccelerometerPage retrieves the AccelerometerProvider instance using Provider.of&ltAccelerometerProvider>(context) and accesses its properties (x, y, z, and isShaking) to update the UI.

Error Handling and Permissions

Real-world applications need robust error handling. Here’s how to check for sensor availability and handle permission requests.

Step 1: Check Sensor Availability

Before using a sensor, check if it’s available on the device.

import 'package:sensors_plus/sensors_plus.dart';

Future&ltbool> checkAccelerometerAvailability() async {
  bool isAvailable = await accelerometerEvents.first != null;
  return isAvailable;
}

Step 2: Request Permissions

On some platforms, you need to request sensor permissions. The permission_handler package is a popular choice. Add it to your pubspec.yaml:

dependencies:
  permission_handler: ^10.0.0

Then, request permissions:

import 'package:permission_handler/permission_handler.dart';

Future&ltvoid> requestSensorPermissions() async {
  var status = await Permission.sensors.status;
  if (!status.isGranted) {
    status = await Permission.sensors.request();
    if (status.isGranted) {
      print('Sensor permission granted');
    } else {
      print('Sensor permission not granted');
      // Handle the case where the user denies permission
    }
  }
}

Advanced Usage and Best Practices

  • Sensor Sampling Rates: The sensors_plus package allows setting different sensor sampling rates. Use this wisely to balance accuracy with battery life.
  • Data Filtering: Apply low-pass filters to smooth the accelerometer data, reducing noise. Libraries like kalman_filter can be useful.
  • Combine Sensors: Fuse accelerometer data with data from other sensors (e.g., gyroscope, magnetometer) for more accurate motion tracking. Sensor fusion algorithms like the Madgwick filter are useful here.

Conclusion

Integrating device sensors like the accelerometer in Flutter unlocks exciting possibilities for creating innovative and interactive applications. By leveraging packages like sensors_plus and employing robust state management and error handling techniques, developers can create powerful and user-friendly experiences that respond intelligently to device motion and orientation.