Flutter Modena Blog
February 13, 2023

Ossigenio

di Davide Palma
 •  7   • 1326 
Table of contents

Ossigenio è una piattaforma per monitorare la qualità dell’aria nelle proprie vicinanze e per scoprire i luoghi di studio con la qualità dell’aria migliore.

È stata creata per il progetto universitario di IoT. Qui il codice sorgente.

Requisiti del progetto

Essendo un progetto di IoT, deve essere composto da un dispositivo IoT embedded, come un sensore, un “bridge” che colleghi il dispositivo a Internet e ne trasmetta i dati e un server che li riceva.

Abbiamo deciso quindi di utilizzare, per soddisfare i requisiti del progetto, un sensore IoT basato su ESP32 che si vuole collegare, tramite Bluetooth Low Energy , a uno smartphone, che dovrà inviare poi i dati geolocalizzati su cloud.

Abbiamo quindi bisogno di una applicazione che ci gestisca:

… il tutto in maniera cross platform.

Perché usare Flutter

Dopo una breve ricerca, abbiamo deciso di utilizzare Flutter per costruire l’applicazione. Ecco perché:

I vantaggi di Flutter sono stati scoperti in corso d’opera, ci ha stupito soprattutto la performance e la null safety (di Dart)

Tempistiche

Partendo da zero esperienza in sviluppo app, il prototipo funzionante ha richiesto circa due mesi di sviluppo. Ciò sottolinea la semplicità di apprendimento di Dart e di Flutter. Lo sviluppo, chiaramente, sarebbe stato ancora più rapido se avessimo già conosciuto le logiche tipiche della programmazione asincrona e della UI dichiarativa (I due pilastri di Flutter).

Struttura dell’app

Non essendo uno sviluppatore professionista, ho strutturato le cose seguendo l’intuito, il che significa che i pattern di sviluppo migliori potrebbero essere diversi.

Il codice Dart dell’applicazione è diviso in: - Classi Managers, che svolgono il lavoro “dietro le quinte”; - Widgets, quindi codice che riguarda la UI - Varie classi di utilities

Spieghiamo nel dettaglio i Manager e la UI.

Managers

Nell’ambito di questo progetto, i manager sono classi singleton che implementano vari metodi per gestire una certa funzionalità. Essi sono: - account_man , gestisce la comunicazione col server e le API HTTP REST - ble_man , gestisce il Bluetooth e la comunicazione con i sensori - gps_man , gestisce il GPS e la posizione dell’utente - mqtt_man , gestisce la comunicazione con il cloud MQTT - perm_man , chiede i permessi necessari all’utente (Bluetooth e GPS) - pref_man. , salva alcuni dati nella memoria permanente.

UI

In Flutter, ogni elemento della UI è un widget. Ogni widget può eventualmente contenere uno o più widget al suo interno. Ciò struttura la UI come un widget tree.
1c088cd0fbed484534f1dfdf0b0e8caa.png

A piacimento si può prendere una parte dell’albero desiderato e creare un custom widget. È stato deciso quindi di dividere i widget in tre categorie:

Pagine

2bf1ea36b9f6271dd6426a2ef7da5da7.png

Le pagine rappresentano una vista completa dell’app, quindi la schermata principale, le schermate di login e registrazione e le schermate di previsione.

Schede

La pagina principale è composta dalla scheda di benvenuto e la scheda per visualizzare la mappa.

e4e88008e27a7c0960ada54a616fb54e.png

Widget

I vari widget “di base”, quindi le card presenti nella schermata principale.

452db2f03d4405d527c9c99e18af9745.png

6aaaef16550b477bf9949acf1b49c34b.png

UI <-> Managers

I meccanismi utilizzati in larga scala per far comunicare la UI con la business logic, oltre le classiche Future e FutureBuilder , sono:

Segue una spiegazione più dettagliata di questi meccanismi.

StreamController

Gli StreamController incapsulano uno Stream e permettono di aggiungerci valori. Gli Stream sono utili in cui si hanno diversi valori restituiti in tempi non definiti. Per esempio, lo StreamController è stato usato per i risultati della scansione Bluetooth del dispositivo, per la posizione dell’utente dal GPS e per i messaggi ricevuti dal sensore.

/// La lista dei messaggi scambiati col dispositivo
final List<MessageWithDirection> messages = [];
late Stream<MessageWithDirection> messagesStream;

Lo stream dei messaggi viene definito durante l’inizializzazione della classe Device:

messagesStream = btUart.txCharacteristic.value
        .map((value) {
          // Crea un messaggio da un valore
          final Message? message = SerialComm.receive(value);
          if (message != null) {
            return MessageWithDirection(
                MessageDirection.received, DateTime.now(), message);
          }
          return null;
        })
        .where((message) => message != null)
        .cast<MessageWithDirection>()
        .asBroadcastStream();

Quando viene aggiunto un valore allo stream, tutti i callback registrati vengono eseguiti. Un callback si registra con il metodo listen:

    messagesStream.listen((message) {
      Log.v("Message received");
      // Salva i messaggi scambiati
      messages.add(message);
      // Manda i messaggi ricevuti su MQTT
      if (message.direction == MessageDirection.received) {
        MqttManager().publish(message.message);
      }
    });

Non bisogna quindi effettuare alcun polling. Bisogna ricordarsi che gli Stream non conservano valori, quindi non sono adatti a valori che vengono restituiti di rado o solamente una volta.

ValueNotifier

In Flutter, per aggiornare lo stato di uno StatefulWidget , è necessario utilizzare il metodo setState all’interno della classe State. Non possiamo però chiamare questo metodo in altre classi o altri file, in quanto necessita di un BuildContext. In altre parole, se cambia il valore di una variabile che si trova in un’altra classe, la UI non verrà aggiornata. Per risolvere questo problema, c’è ValueNotifier e ValueListenableBuilder , rispettivamente una classe che conserva un singolo valore e che avvisa i suoi Listener quando il valore cambia e un widget che ascolta un ValueNotifier.

  /// Il dispositivo a cui siamo connessi
  ValueNotifier<Device?> dvc = ValueNotifier<Device?>(null);

Nella parte di connessione al dispositivo, imposto il valore:

  /// Prova a connettersi a un dispositivo.
  ///
  /// Questo metodo cerca di connettersi al dispositivo.
  /// Lo imposta in [dvc] se la connessione è andata a buon fine.
  Future<void> connectToDevice(ScanResult result) async {
    stopBLEScan();

    try {
      await result.device.connect().timeout(const Duration(seconds: 3));
    } on Exception catch (e) {
      Log.l("Errore durante la connessione: $e");
      rethrow;
    }

    dvc.value = Device(result, await BTUart.fromDevice(result.device));
    return;
  }

Nella UI, infine, mostro la card relativa al sensore:

  ValueListenableBuilder(
     valueListenable: BLEManager().dvc,
     builder: (context, dvc, _) {
         // Se non siamo connessi a un dispositivo, ritorniamo una
         // scatola vuota e invisibile
       if (dvc == null) {
         return const SizedBox();
       }
       return UI.buildCard(AirQualityDevice(device: dvc));
     }),

Conclusioni

Flutter non solo si è mostrato all’altezza delle aspettative, ma le ha ampiamente superate, dimostrando di essere un framework che non ha paura di nulla.

Tuttavia, come in ogni tecnologia, ho notato aree di potenziale miglioramento:

Entrambe le librerie supportano solo iOS e Android.

Siccome abbiamo bisogno solamente della parte Low Energy del bluetooth, se potessimo tornare indietro avremmo scelto quick_blue , dato che supporta anche le piattaforme desktop.

Nota di “redazione”: Questo è il primo degli articoli pubblicati da membri del nostro meetup.

In questo caso Davide, dopo aver presentato il suo progetto durante il nostro incontro di Febbraio, ha anche contribuito questo articolo per il nostro blog, così da poter condividere la sua esperienza con quelli che non c’erano quel giorno.

Se non sei ancora parte di Flutter Modena, entra nel nostro gruppo Telegram e iscriviti su Meetup per sapere dei prossimi eventi.

Se invece sei dei nostri, e vuoi raccontarci la tua esperienza con un articolo e/o con una presentazione durante uno dei nostri eventi, contatta uno di noi organizzatori e parliamone!

Seguici!

Seguici sui social o entra nel nostro gruppo Telegram!