[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-sensori-di-movimento-in-flutter-con-sensors-plus-accelerometro-giroscopio-e-shake-detection":3,"comments-article-sensori-di-movimento-in-flutter-con-sensors-plus-accelerometro-giroscopio-e-shake-detection":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},113,"Sensori di movimento in Flutter con sensors_plus: accelerometro, giroscopio e shake detection","sensori-di-movimento-in-flutter-con-sensors-plus-accelerometro-giroscopio-e-shake-detection","Accelerometro, giroscopio e magnetometro aprono la porta a interazioni originali: scuotere il telefono, inclinare la UI, creare una livella. Vediamo come usare sensors_plus in modo corretto, con filtri, gestione del ciclo di vita e attenzione ai consumi.","## Perché usare i sensori di movimento\n\nQuasi ogni smartphone integra accelerometro, giroscopio e magnetometro. Sono sensori che usiamo ogni giorno senza accorgercene (rotazione dello schermo, bussola, contapassi) ma che in un'app Flutter possono diventare un elemento di interazione interessante:\n\n- **shake to undo \u002F shake to report**: scuotere il dispositivo per annullare un'azione o aprire un form di segnalazione bug;\n- **effetti parallax**: sfondi o card che si muovono leggermente inclinando il telefono;\n- **livella e inclinometro**: strumenti di misura;\n- **rilevamento di attività**: capire se il dispositivo è fermo, in tasca o in movimento;\n- **giochi**: controllo del personaggio tramite inclinazione.\n\nIl pacchetto ufficiale del team Flutter Community per accedere a questi dati è **`sensors_plus`**, parte della famiglia `plus_plugins`. In questo articolo vediamo come integrarlo correttamente, come filtrare i dati (che sono sempre rumorosi) e come evitare i due errori più comuni: subscription mai cancellate e consumo eccessivo di batteria.\n\n## Installazione e configurazione\n\nAggiungi la dipendenza:\n\n```bash\nflutter pub add sensors_plus\n```\n\nSu **iOS** non serve alcun permesso per accelerometro e giroscopio (solo il movimento con `CMMotionActivity` richiederebbe `NSMotionUsageDescription`, non usato da `sensors_plus` per questi stream).\n\nSu **Android**, se imposti frequenze di campionamento superiori a 200 Hz e il target è Android 12 (API 31) o superiore, devi dichiarare il permesso nel `AndroidManifest.xml`:\n\n```xml\n\u003Cuses-permission android:name=\"android.permission.HIGH_SAMPLING_RATE_SENSORS\" \u002F>\n```\n\nPer i casi d'uso tipici (UI reattiva, shake detection) non serve: le frequenze normali sono più che sufficienti.\n\n## I sensori disponibili\n\n`sensors_plus` espone diversi stream, ognuno con un proprio significato fisico. Confonderli è il primo errore da evitare.\n\n| Stream | Cosa misura | Unità |\n| --- | --- | --- |\n| `accelerometerEventStream()` | Accelerazione **comprensiva della gravità** | m\u002Fs² |\n| `userAccelerometerEventStream()` | Accelerazione **al netto della gravità** | m\u002Fs² |\n| `gyroscopeEventStream()` | Velocità angolare sui tre assi | rad\u002Fs |\n| `magnetometerEventStream()` | Campo magnetico (utile per la bussola) | microtesla |\n| `barometerEventStream()` | Pressione atmosferica (se il sensore esiste) | hPa |\n\nIl sistema di riferimento è solidale al dispositivo: **x** da sinistra a destra, **y** dal basso verso l'alto dello schermo, **z** uscente dallo schermo verso l'utente. Con il telefono appoggiato sul tavolo, `accelerometerEventStream()` restituisce circa `(0, 0, 9.81)`, mentre `userAccelerometerEventStream()` restituisce valori vicini a zero.\n\n> Regola pratica: se ti interessa **l'orientamento** del dispositivo usa l'accelerometro \"completo\"; se ti interessa il **movimento impresso dall'utente** usa `userAccelerometer`.\n\n## Lettura base dei dati\n\nGli stream sono `Stream` broadcast: puoi ascoltarli con `StreamBuilder` oppure con una `StreamSubscription` esplicita. La seconda opzione è preferibile quando devi elaborare i dati prima di mostrarli.\n\n```dart\nimport 'dart:async';\n\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:sensors_plus\u002Fsensors_plus.dart';\n\nclass SensorDemo extends StatefulWidget {\n  const SensorDemo({super.key});\n\n  @override\n  State\u003CSensorDemo> createState() => _SensorDemoState();\n}\n\nclass _SensorDemoState extends State\u003CSensorDemo> {\n  StreamSubscription\u003CAccelerometerEvent>? _sub;\n  AccelerometerEvent? _last;\n\n  @override\n  void initState() {\n    super.initState();\n    _sub = accelerometerEventStream(\n      samplingPeriod: SensorInterval.uiInterval,\n    ).listen(\n      (event) => setState(() => _last = event),\n      onError: (Object error) {\n        \u002F\u002F Il sensore potrebbe non esistere (emulatore, desktop, web)\n        debugPrint('Sensore non disponibile: $error');\n      },\n      cancelOnError: true,\n    );\n  }\n\n  @override\n  void dispose() {\n    _sub?.cancel(); \u002F\u002F fondamentale\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final e = _last;\n    return Center(\n      child: Text(\n        e == null\n            ? 'In attesa di dati...'\n            : 'x: ${e.x.toStringAsFixed(2)}\\n'\n              'y: ${e.y.toStringAsFixed(2)}\\n'\n              'z: ${e.z.toStringAsFixed(2)}',\n        textAlign: TextAlign.center,\n      ),\n    );\n  }\n}\n```\n\nDue dettagli importanti:\n\n1. **`onError` non è opzionale nella pratica**: su emulatori, su Flutter Web e su alcuni dispositivi desktop il sensore non esiste e lo stream emette un errore. Senza handler, l'app crasha.\n2. **`dispose()` deve sempre cancellare la subscription**, altrimenti il plugin continua a ricevere eventi dal sistema operativo anche quando la schermata non è più visibile.\n\n### Scegliere la frequenza di campionamento\n\nIl parametro `samplingPeriod` accetta una `Duration`. Le costanti di `SensorInterval` coprono i casi standard:\n\n```dart\nSensorInterval.normalInterval; \u002F\u002F ~200 ms — cambi di orientamento\nSensorInterval.uiInterval;     \u002F\u002F ~60 ms  — animazioni UI\nSensorInterval.gameInterval;   \u002F\u002F ~20 ms  — giochi\nSensorInterval.fastestInterval;\u002F\u002F il più veloce possibile\n```\n\nPiù alta è la frequenza, maggiore è il consumo di CPU e batteria. Per un effetto parallax `uiInterval` è già abbondante; per una shake detection `gameInterval` offre reattività migliore senza esagerare. Evita `fastestInterval` se non stai facendo elaborazione di segnale seria.\n\n## Caso pratico 1: shake detection\n\nIl rilevamento dello \"scuotimento\" è il caso d'uso più richiesto. L'algoritmo è semplice: si calcola il **modulo** dell'accelerazione utente e, se supera una soglia, si registra un evento. Per evitare falsi positivi si applica un **debounce temporale** e si può richiedere un numero minimo di picchi ravvicinati.\n\n```dart\nimport 'dart:async';\nimport 'dart:math' as math;\n\nimport 'package:sensors_plus\u002Fsensors_plus.dart';\n\nclass ShakeDetector {\n  ShakeDetector({\n    required this.onShake,\n    this.threshold = 12.0, \u002F\u002F m\u002Fs^2 sul modulo dell'accelerazione utente\n    this.minGapBetweenShakes = const Duration(milliseconds: 800),\n  });\n\n  final VoidCallback onShake;\n  final double threshold;\n  final Duration minGapBetweenShakes;\n\n  StreamSubscription\u003CUserAccelerometerEvent>? _sub;\n  DateTime _lastShake = DateTime.fromMillisecondsSinceEpoch(0);\n\n  void start() {\n    _sub ??= userAccelerometerEventStream(\n      samplingPeriod: SensorInterval.gameInterval,\n    ).listen(_onEvent, onError: (_) {});\n  }\n\n  void _onEvent(UserAccelerometerEvent event) {\n    final magnitude = math.sqrt(\n      event.x * event.x + event.y * event.y + event.z * event.z,\n    );\n\n    if (magnitude \u003C threshold) return;\n\n    final now = DateTime.now();\n    if (now.difference(_lastShake) \u003C minGapBetweenShakes) return;\n\n    _lastShake = now;\n    onShake();\n  }\n\n  Future\u003Cvoid> stop() async {\n    await _sub?.cancel();\n    _sub = null;\n  }\n}\n```\n\n`VoidCallback` arriva da `package:flutter\u002Ffoundation.dart`. Usalo così in un widget:\n\n```dart\nlate final ShakeDetector _detector;\n\n@override\nvoid initState() {\n  super.initState();\n  _detector = ShakeDetector(\n    onShake: () {\n      ScaffoldMessenger.of(context).showSnackBar(\n        const SnackBar(content: Text('Scuotimento rilevato!')),\n      );\n    },\n  )..start();\n}\n\n@override\nvoid dispose() {\n  _detector.stop();\n  super.dispose();\n}\n```\n\n**Come tarare la soglia**: 12 m\u002Fs² è un buon punto di partenza per uno scuotimento volontario. Scendendo sotto 8 rischi falsi positivi mentre si cammina; sopra 20 servono movimenti molto bruschi. La cosa migliore è costruire una schermata di debug che mostra il modulo in tempo reale e provarla su device reali.\n\n## Caso pratico 2: effetto parallax con filtro passa-basso\n\nI dati grezzi dell'accelerometro sono **rumorosi**: se li colleghi direttamente a una `Transform`, la UI trema. La soluzione classica è un filtro passa-basso (media esponenziale), che smussa i valori mantenendo la reattività.\n\n```dart\nclass _ParallaxCardState extends State\u003CParallaxCard> {\n  StreamSubscription\u003CAccelerometerEvent>? _sub;\n  double _dx = 0;\n  double _dy = 0;\n\n  static const _alpha = 0.15; \u002F\u002F più basso = più morbido\n  static const _maxOffset = 16.0; \u002F\u002F pixel\n\n  @override\n  void initState() {\n    super.initState();\n    _sub = accelerometerEventStream(\n      samplingPeriod: SensorInterval.uiInterval,\n    ).listen((event) {\n      \u002F\u002F Normalizzo rispetto a g e limito l'escursione\n      final targetX = (-event.x \u002F 9.81).clamp(-1.0, 1.0) * _maxOffset;\n      final targetY = (event.y \u002F 9.81).clamp(-1.0, 1.0) * _maxOffset;\n\n      setState(() {\n        _dx = _dx + _alpha * (targetX - _dx);\n        _dy = _dy + _alpha * (targetY - _dy);\n      });\n    }, onError: (_) {});\n  }\n\n  @override\n  void dispose() {\n    _sub?.cancel();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Transform.translate(\n      offset: Offset(_dx, _dy),\n      child: widget.child,\n    );\n  }\n}\n```\n\nSe l'effetto interessa solo una porzione della schermata, `setState` sull'intero widget è sprecato: meglio esporre un `ValueNotifier\u003COffset>` e avvolgere solo la parte animata in un `ValueListenableBuilder`, così il rebuild resta confinato.\n\n```dart\nfinal _offset = ValueNotifier\u003COffset>(Offset.zero);\n\n\u002F\u002F nel listener\n_offset.value = Offset(newDx, newDy);\n\n\u002F\u002F nel build\nValueListenableBuilder\u003COffset>(\n  valueListenable: _offset,\n  builder: (context, offset, child) =>\n      Transform.translate(offset: offset, child: child),\n  child: const HeavyBackground(),\n)\n```\n\n## Caso pratico 3: calcolare l'inclinazione (livella)\n\nDall'accelerometro si ricavano facilmente **pitch** e **roll**, cioè l'inclinazione del dispositivo rispetto al piano orizzontale:\n\n```dart\nimport 'dart:math' as math;\n\n({double pitch, double roll}) tiltFrom(AccelerometerEvent e) {\n  final pitch = math.atan2(-e.y, math.sqrt(e.x * e.x + e.z * e.z));\n  final roll = math.atan2(e.x, e.z);\n  return (\n    pitch: pitch * 180 \u002F math.pi,\n    roll: roll * 180 \u002F math.pi,\n  );\n}\n```\n\nIl valore restituito è un record di Dart 3 con i gradi. Anche qui conviene applicare un filtro passa-basso prima di mostrare il numero, altrimenti la lettura oscilla di continuo.\n\nPer ottenere invece l'**heading** (la direzione della bussola) serve combinare accelerometro e magnetometro con una matrice di rotazione: se ti basta la bussola, valuta pacchetti dedicati come `flutter_compass`, che incapsulano già la fusione dei sensori e la correzione della declinazione magnetica.\n\n## Fermare i sensori quando l'app va in background\n\nUn'app che continua a campionare l'accelerometro mentre è in background consuma batteria inutilmente (e su iOS potrebbe comunque non ricevere eventi). La soluzione più pulita è `AppLifecycleListener`:\n\n```dart\nlate final AppLifecycleListener _lifecycle;\n\n@override\nvoid initState() {\n  super.initState();\n  _detector.start();\n  _lifecycle = AppLifecycleListener(\n    onPause: () => _detector.stop(),\n    onResume: () => _detector.start(),\n  );\n}\n\n@override\nvoid dispose() {\n  _lifecycle.dispose();\n  _detector.stop();\n  super.dispose();\n}\n```\n\nLo stesso ragionamento vale per le schermate: se una pagina con sensori attivi viene coperta da un'altra route, valuta di sospendere lo stream con un `RouteAware` o un semplice flag legato alla visibilità.\n\n## Rendere i sensori testabili\n\nDipendere direttamente dalle funzioni globali di `sensors_plus` rende difficile scrivere unit test. La soluzione è nascondere la sorgente dietro un'astrazione:\n\n```dart\nabstract interface class MotionSource {\n  Stream\u003CUserAccelerometerEvent> get userAcceleration;\n}\n\nclass SensorsPlusMotionSource implements MotionSource {\n  @override\n  Stream\u003CUserAccelerometerEvent> get userAcceleration =>\n      userAccelerometerEventStream(\n        samplingPeriod: SensorInterval.gameInterval,\n      );\n}\n\nclass FakeMotionSource implements MotionSource {\n  final _controller = StreamController\u003CUserAccelerometerEvent>.broadcast();\n\n  @override\n  Stream\u003CUserAccelerometerEvent> get userAcceleration => _controller.stream;\n\n  void emit(double x, double y, double z) => _controller.add(\n        UserAccelerometerEvent(x, y, z, DateTime.now()),\n      );\n}\n```\n\nNel test puoi così simulare uno scuotimento e verificare che la callback venga invocata una sola volta grazie al debounce, senza toccare hardware reale. Nota che il costruttore degli eventi di `sensors_plus` richiede anche un `timestamp`: se usi una versione precedente alla 5, il parametro potrebbe non essere presente, quindi verifica la firma nella tua versione.\n\n## Errori comuni da evitare\n\n- **Dimenticare `cancel()`**: è la causa numero uno di memory leak e di batteria che si scarica.\n- **Confondere accelerometro e userAccelerometer**: se la tua soglia di shake viene superata anche con il telefono fermo, stai quasi certamente misurando la gravità.\n- **Aggiornare la UI a 200 Hz**: il rendering non ne beneficia e il jank aumenta. Campiona veloce solo se elabori i dati, poi aggiorna la UI a frequenza ridotta (per esempio con un throttle).\n- **Assumere che i sensori esistano sempre**: emulatori, Flutter Web e molti PC desktop non li espongono. Prevedi sempre un fallback nell'interfaccia.\n- **Ignorare l'orientamento dello schermo**: in landscape gli assi restano solidali al dispositivo, non alla UI. Se la tua logica dipende dall'orientamento, leggi `MediaQuery.orientationOf(context)` e scambia gli assi di conseguenza.\n- **Usare i sensori per funzionalità critiche**: la precisione varia enormemente tra dispositivi economici e di fascia alta. Vanno bene per interazioni accessorie, non per misure di precisione.\n\n## Conclusioni\n\n`sensors_plus` è un pacchetto piccolo e con un'API essenziale: qualche funzione che restituisce stream, nient'altro. La complessità non sta nel plugin ma in ciò che ci costruisci sopra — filtraggio del rumore, taratura delle soglie, gestione del ciclo di vita e dei consumi.\n\nSe adotti fin da subito tre regole (cancellare sempre le subscription, filtrare i dati prima di mostrarli, sospendere il campionamento in background) i sensori diventano uno strumento affidabile per aggiungere un tocco di fisicità alle tue app, dallo shake-to-report agli effetti di profondità che rendono un'interfaccia più viva.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F7f478e76-d802-40e0-a49c-49fcec90cae5.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1626478233437-3f093ad8ad68?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA3NDA4OTl8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Itadaki","https:\u002F\u002Funsplash.com\u002F@itadakidesu","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fblack-and-gray-device-on-brown-wooden-table-X_OCyXkKcOU",null,"published","2026-09-30T04:01:40+00:00","Sensori di movimento in Flutter con sensors_plus","Guida pratica a sensors_plus: accelerometro, giroscopio, shake detection, effetti parallax, filtri anti-rumore e gestione corretta di batteria e lifecycle.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316908319]