Sviluppare un’applicazione Flutter – Parte 1

Questo sarà il primo di una serie di articoli dove andremo a realizzare passo passo un’applicazione mobile multipiattaforma, Android ed iOS, con le seguenti funzionalità:

  • Supporto multilingua;
  • Autenticazione tramite uno strato API web con relativa gestione del token di autenticazione per eseguire chiamate API autenticate;
  • Base dati SQLite locale con salvataggio ed interrogazione di dati.

Vedremo inoltre alcune finezze per l’abbellimento finale ed il miglioramento della nostra applicazione, come ad esempio il supporto alla modalità scura.

Il tutto verrà realizzato tramite il framework open source Flutter ed il linguaggio di programmazione Dart.

Vediamolo più nel dettaglio…

Che cos’è Flutter?

Flutter, direttamente dal sito ufficiale, è un framework open source realizzato da Google, per lo sviluppo di applicazioni multi piattaforma, compilate nativamente, partendo da un singolo codice sorgente. Per intenderci, un rivale del framework Ionic.

Per chi ha già utilizzato Ionic, appunto, ecco una tabella comparativa che ne evidenzia le differenze:

I due framework sono molto simili dal punto di vista di ciò che viene offerto, community, pacchetti disponibili e dalla visione condivisa che hanno, un solo codice sorgente, deploy mobile, desktop e web.

La scelta quindi, più che da un esigenza di progetto, può essere dettata meramente da gusto e/o conoscenze personali.

Requisiti ed installazione

Per iniziare a sviluppare utilizzando Flutter, bisogna prima eseguire l’installazione dell’SDK sulla macchina che stiamo utilizzando. L’SDK è disponibile per diversi sistemi operativi: Windows, macOS, Linux e Chrome OS (link).

In questa guida seguiremo la procedura per l’installazione su una macchina con sistema operativo Windows.

Iniziamo…

Per prima cosa assicuriamoci di soddisfare i requisiti minimi richiesti per eseguire l’installazione:

  • Windows 7 SP1 o superiore (64bit) come sistema operativo;
  • Almeno 1,64 GB disponibili sul disco per l’SDK Flutter;
  • Windows PowerShell 5.0 o superiore (preinstallato se utilizzate Windows 10/11);
  • Visual Studio Code come editor;
  • Android Studio + SDK Android per eseguire il deploy come applicazione Android su emulatore o su dispositivo fisico.

Verificati ed allineati ai requisiti richiesti, scarichiamo l’ultima versione stabile di Flutter per Windows (all’istante di scrittura di questa guida 2.10.4) dal seguente link.

Estraiamo il contenuto dell’archivio in una cartella di nostra preferenza, per esempio, C:\src\flutter.

Nota: il percorso in cui viene estratto l’archivio non deve contenere caratteri speciali e non deve essere estratto in cartelle che richiedono privilegi elevati per la scrittura su di esse, come ad esempio la cartella C:\Program Files\.

Per poter utilizzare i comandi Flutter all’interno del prompt dei comandi di Windows, aggiungiamo il percorso alla cartella flutter\bin estratta in precedenza, alla variabile d’ambiente Path dell’utente corrente.

Riavviamo il prompt dei comandi, se già avviato, e lanciamo il comando flutter doctor per verificare che tutti gli strumenti siano installati e funzionino correttamente. Dovremo ottenere una risposta simile alla seguente:

Il comando ci mostra se tutti gli strumenti sono installati e configurati correttamente. Nel caso sopra ci viene mostrato per esempio un problema con il software Visual Studio Enterprise 2022 in quando manca il componente Desktop development with C++ necessario per il deploy di applicazioni Windows (non utile però per la nostra applicazione Android al momento).

I punti che ci interessano sono:

  • Flutter;
  • Android toolchain;
  • Android Studio;
  • VS Code.

Se questi punti sono confermati con il checkmark ✔ che indica OK, possiamo proseguire.

Visual Studio Code

Per lo sviluppo di un applicativo Flutter, la documentazione ufficiale propone tre strumenti ideali: Android Studio con IntelliJVisual Studio Code ed Emacs.

Noi, per questa serie di articoli, utilizzeremo Visual Studio Code.

Apriamo quindi VSCode e rechiamoci alla sezione delle estensioni aggiuntive, dal pannello laterale sinistro. Cerchiamo, scrivendo nella barra di ricerca, l’estensione Flutter ed installiamola:

Terminata l’installazione, dalla Command Palette… (Ctrl+Shift+P) cerchiamo e lanciamo il comando Flutter: Run Flutter Doctor.

Nel caso di errori o problematiche ci verranno mostrati degli avvisi con una proposta di intervento da attuare per correggere il problema. Se non viene mostrato nulla, invece, il comando è andato a buon fine e siamo pronti per iniziare un nuovo progetto Flutter.

Creazione del progetto

Dalla Command Palette… (Ctrl+Shift+P) di VSCode, cerchiamo e lanciamo il comando Flutter: New Project.

Selezioniamo successivamente Application.

Dalla finestra popup selezioniamo la cartella dove verrà salvato il nuovo progetto. Per esempio C:\repos\demo_flutter.

Infine, quando richiesto, il nome dell’applicazione. Nel nostro esempio demo_application.

Terminata la procedura ci troveremo davanti il progetto appena creato:

Proviamo a compilare ed eseguire l’applicazione per verificare se tutto funziona correttamente.

Per farlo selezioniamo l’emulatore Android o il dispositivo fisico collegato tramite ADB (USB o Wifi) nel pannello in basso a destra di VSCode:

Avviamo poi l’applicazione, menu Run, Start Debuggging o più semplicemente il tasto F5 sulla tastiera.

Se otterrete la schermata sopra, tutto funziona come deve.

La funzionalità del linguaggio Dart Hot Reload è abilitata di default, modificando quindi una stringa nel codice, per esempio la stringa “Flutter Demo Home Page” in “Demo Application“, noteremo che la modifica viene mostrata sul dispositivo (emulato o fisico) direttamente, senza necessità di ricompilare o rieseguire l’app.

Prima di proseguire nello sviluppo della nostra applicazione, vediamo che strumenti di debug Flutter ci mette a disposizione.

Dart DevTools

Per eseguire il debug completo dell’applicazione, possiamo utilizzare Dart DevTools, uno strumento di debugging del tutto simile, per esempio, ai Chrome DevTools che si utilizzano nello sviluppo Web su Google Chrome o nello sviluppo di applicazioni Ionic (per chi venisse da quel mondo).

Nel pannello in basso a destra di VSCode è presente un collegamento rapido per accederci, Dart DevTools, clicchiamolo e selezioniamo Open DevTools in Web Browser.

VI rimando al sito ufficiale per approfondire lo strumento.

Struttura del progetto

Vediamo ora più nel dettaglio come è strutturato un progetto Flutter.

Struttura di un progetto di tipo Application appena creato

Le cartelle android ed ios contengono i progetti rispettivi Android Studio ed XCode.

Le cartelle web e windows, invece, contengono i progetti per il deploy Web o Windows appunto.

La cartella lib contiene la logica dell’applicazione, quella che andremo a sviluppare per intenderci, quindi tutti i file .dart in linguaggio Dart e i file .arb per le traduzioni per esempio (queste le vedremo nella seconda parte di questa serie di articoli).

Questa cartella, per ottimizzare la lettura del codice, può essere a sua volta suddivisa come segue:

  • pages – i file .dart che identificano le pagine dell’applicazione. Per esempio: login.dartmain.dart, ecc…;
  • components – i file .dart che identificano componenti che vengono riutilizzati all’interno delle pagine sopra per esempio. Se abbiamo un oggetto che utilizziamo più volte in più pagine dell’applicativo, è buona norma estrapolare la sua logica dalla logica della pagina stessa e creare un componente esterno, magari parametrizzabile per personalizzazioni future, evitando così ridondanza di codice;
  • l10n – può contenere i file .arb in lingua per le traduzioni. Per esempio intl_en.arbintl_it.arb, ecc…;
  • providers – contenente i file .dart delle varie classi singleton utilizzate in vari punti dell’applicazione. In questa sezione rientra per esempio il provider delle API web api_provider.dart (che vedremo nei prossimi articoli).

Il file pubspec.yaml definisce alcuni aspetti del progetto, dal nome, la descrizione, i pacchetti dipendenza del progetto, fino ad alcune impostazioni di Flutter come per esempio l’opzione uses-material-design che indica se l’applicativo deve usare o meno il Material Design per l’interfaccia grafica.
Questo file è comparabile al file package.json di un progetto Angular.

Iniziamo con lo sviluppo vero e proprio…

Pagina di Login

Creiamo all’interno della cartella lib una nuova cartella pages ed al suo interno creiamo un nuovo file chiamato login.dart. Questo file conterrà la logica della nostra pagina di autenticazione.

All’interno del file login.dart importiamo il componente Material:

import 'package:flutter/material.dart';

e creiamo una nuova classe chiamata LoginPage che estende una classe astratta StatefulWidget (questa classe astratta a sua volta estende un’oggetto Widget. Tutti gli elementi grafici in Flutter sono Widget) come segue:

class LoginPage extends StatefulWidget {
  const LoginPage({Key? key}) : super(key: key);
}

Creiamo ora, sempre all’interno dello stesso file, una classe che estende lo stato del nostro nuovo StatefulWidget LoginPage:

class _LoginState extends State<LoginPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Login"),
      ),
      body: const Center(
        child: Text("Demo"),
      ),
    );
  }
}

Torniamo poi all’interno della classe LoginPage ed implementiamo il metodo createState() indicando la nuova classe stato _LoginState:

class LoginPage extends StatefulWidget {
  const LoginPage({Key? key}) : super(key: key);

  @override
  State<LoginPage> createState() => _LoginState();
}

A questo punto abbiamo creato una nuova classe LoginPage che, istanziata, disegna un’oggetto Scaffold (oggetto che indica una pagina in stile Material) con un’app bar contenente un titolo (stringa “Login” nel nostro esempio) ed un corpo con un’oggetto di tipo testo al centro (stringa “Demo”).

Ora non resta che impostare l’applicazione in modo tale da avviare la pagina di autenticazione LoginPage anziché l’attuale pagina MyHomePage all’avvio.

Per fare questo apriamo il file main.dart all’interno dell’editor ed eliminiamo le classi MyHomePage e _MyHomePageState.

Importiamo il componente login.dart:

import 'package:demo_application/pages/login.dart';

ed impostiamo la proprietà home dell’oggetto MaterialApp con l’oggetto LoginPage creato in precedenza:

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // Try running your application with "flutter run". You'll see the
        // application has a blue toolbar. Then, without quitting the app, try
        // changing the primarySwatch below to Colors.green and then invoke
        // "hot reload" (press "r" in the console where you ran "flutter run",
        // or simply save your changes to "hot reload" in a Flutter IDE).
        // Notice that the counter didn't reset back to zero; the application
        // is not restarted.
        primarySwatch: Colors.blue,
      ),
      home: const LoginPage(),
    );
  }
}

Eseguendo l’applicazione con F5 otteniamo il seguente risultato:

Utilizzando i Widget a nostra disposizione, disegniamo un modulo form di autenticazione:

class _LoginState extends State<LoginPage> {
  String? _email;
  String? _password;

  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  Widget _buildEmail() {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: TextFormField(
        keyboardType: TextInputType.emailAddress,
        decoration: const InputDecoration(
          labelText: "Email",
          border: OutlineInputBorder(),
        ),
        validator: (String? value) {
          if (value == null || value.isEmpty) {
            return 'Email richiesta';
          }
          return null;
        },
        onSaved: (String? value) {
          _email = value;
        },
      ),
    );
  }

  Widget _buildPassword() {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: TextFormField(
        obscureText: true,
        decoration: const InputDecoration(
          labelText: "Password",
          border: OutlineInputBorder(),
        ),
        validator: (String? value) {
          if (value == null || value.isEmpty) {
            return 'Password richiesta';
          }
          return null;
        },
        onSaved: (String? value) {
          _password = value;
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Login"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Center(
          child: Form(
            key: _formKey,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const SizedBox(
                  width: 200,
                  height: 100,
                  child: FlutterLogo(),
                ),
                const SizedBox(height: 8),
                _buildEmail(),
                _buildPassword(),
                const SizedBox(height: 8),
                ElevatedButton(
                  onPressed: () {
                    if (!_formKey.currentState!.validate()) {
                      return;
                    }

                    _formKey.currentState!.save();

                    print(_email);
                    print(_password);
                  },
                  child: const SizedBox(
                    width: 200,
                    height: 50,
                    child: Center(
                      child: Text(
                        "Login",
                        textScaleFactor: 1.5,
                      ),
                    ),
                  ),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Eseguendo otteniamo il seguente risultato:

Premendo il pulsante Login il modulo form verrà validato (_formKey.currentState!.validate()):

Inserendo i campi richiesti invece, i valori dei due, Email e Password, verranno stampati all’interno della console:

Vediamo ora più nel dettaglio le parti essenziali del codice scritto sopra.

String? _email;
String? _password;

final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

In questo punto vengono dichiarate le due variabili di tipo String NULLABLE che conterranno i due valori Email e Password e viene dichiarata ed assegnata la variabile di tipo GlobalKey che identifica la chiave univoca dell’oggetto di tipo Form.

Widget _buildEmail() {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: TextFormField(
        keyboardType: TextInputType.emailAddress,
        decoration: const InputDecoration(
          labelText: "Email",
          border: OutlineInputBorder(),
        ),
        validator: (String? value) {
          if (value == null || value.isEmpty) {
            return 'Email richiesta';
          }
          return null;
        },
        onSaved: (String? value) {
          _email = value;
        },
      ),
    );
}

Questo metodo, così come il metodo _buildPassword(), serve per separare la dichiarazione dei vari componenti Widget migliorando così la lettura e manutenzione futura del codice. L’oggetto Widget che ritorna da questo metodo _buildEmail(), può essere scritto direttamente all’interno del metodo build(), rendendone però la lettura molto nidificata e confusionaria.

Qui per esempio viene dichiarato il campo Email, widget TextFormField e le sue proprietà per impostarne il comportamento e la sua visualizzazione a schermo:

  • keyboardType: TextInputType.emailAddress seleziona il tipo di tastiera che il sistema visualizzerà quando il campo viene selezionato. In questo caso verrà mostrata una tastiera con il carattere @ a vista per agevolare l’inserimento di un indirizzo email;
  • labelText: "Email" indica la stringa mostrata come etichetta del campo;
  • border: OutlineInputBorder() indica il tipo di bordo del campo, in questo caso il classico bordo Outlined Material.
  • validator: (String? value) {} contiene la validazione che il valore del campo deve rispettare e il possibile errore mostrato nel caso non venga rispettata.
  • onSaved: (String? value) {} indica le azioni da fare al salvataggio del modulo Form. In questo caso il valore del campo viene assegnato alla variabile _email creata in precedenza.
ElevatedButton(
  onPressed: () {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    _formKey.currentState!.save();

    print(_email);
    print(_password);
  },
  child: const SizedBox(
    width: 200,
    height: 50,
    child: Center(
      child: Text(
        "Login",
        textScaleFactor: 1.5,
      ),
    ),
  ),
)

Il codice sopra disegna all’interno della pagina il pulsante Login ed imposta il suo comportamento, oltre che la sua etichetta.

Invocato l’evento onPressed: () {}, il modulo Form visto sopra, identificato dalla variabile _formKey, viene validato e, se corretto, salvato, così da popolare correttamente le variabili _email e _password (come visto all’interno dei metodi _buildEmail() e _buildPassword()).

Prossimamente…

Nel prossimo articolo vedremo come rendere la nostra applicazione multilingua ed integreremo un provider per interagire con uno strano API web.

GitHub

Il progetto Flutter realizzato in questo articolo è disponibile su GitHub.

Fonti

2 commenti su “Sviluppare un’applicazione Flutter – Parte 1

  1. Pingback: Sviluppare un’applicazione Flutter – Parte 2 | Fontana Marco IT Consulting

  2. Pingback: Sviluppare un’applicazione Flutter – Parte 3 | Fontana Marco IT Consulting

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.