Sviluppare un’applicazione Flutter – Parte 3

Terza parte della serie in cui stiamo realizzando un’applicazione Flutter multipiattaforma da zero, facendolo passo passo.

Fino ad ora, nelle parti precedenti, abbiamo sviluppato un’app multilingua, integrata ad uno strato API Web. Potete trovare la prima e la seconda parte della serie qui sul sito se non le aveste già lette.

Il codice sorgente, inoltre, è disponibile su GitHub separato per articoli.

Ora andremo ad aggiungere al nostro applicativo una nuova pagina con al suo interno una lista di elementi simil applicazione To Do list ed integreremo il tutto con una base dati locale SQLite per la storicizzazione delle informazioni.

Iniziamo…

Pagina Elenco

Creiamo un nuovo file list.dart all’interno della cartella contenente le pagine pages e popoliamolo come segue:

import 'package:flutter/material.dart';

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

  @override
  State<ListPage> createState() => _ListPage();
}

class _ListPage extends State<ListPage> {
  final List<Widget> items = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Elenco"),
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return items[index];
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
           print("On add pressed!");
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Più nel dettaglio, il codice sopra, disegna una pagina Material tramite il Widget Scaffold, come abbiamo già visto precedentemente.

All’interno del corpo della pagina body disegna una lista mediante il Widget ListView, lista popolata poi da un array vuoto items, array di Widget generici al momento.

La pagina contiene anche un FloatingActionButton, il classico pulsantino in stile Material che si trova normalmente nell’angolo in basso a destra della pagina (può essere posizionato anche altrove all’interno della pagina):

Ora dobbiamo poter navigare all’interno di questa nuova pagina. Facciamo si che, effettuando l’accesso mediante la pagina di login, venga mostrata la pagina elenco (nel caso in cui l’accesso vada a buon fine).

Spostiamoci quindi all’interno della pagina di autenticazione, file login.dart, più precisamente all’interno dell’evento onPressed: () {} dell’ElevatedButton di accesso.

Commentiamo l’operazione di autenticazione tramite API vista nell’articolo precedente (al momento non è necessaria per i nostri test):

/*AuthRequest request = AuthRequest(
  email: _email,
  password: _password,
);

GetIt.I<ApiProvider>().auth(request).then((response) {
  print(response?.token);

  if (response != null && response.token != null) {
    setState(() {
      GetIt.I<AuthProvider>().setToken(response.token);
    });
  }
}).onError((error, stackTrace) {
  print(error);
});*/

ed aggiungiamo all’interno dell’evento:

...
print(_email);
print(_password);

...

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const ListPage(),
  ),
);

Le due righe inerenti alla stampa di _email e _password erano già presenti all’interno del codice, servono solo per dare un’indicazione di dove viene inserita la chiamata Navigator API.

La chiamata Navigator API push permette di navigare verso una “strada” indicata tramite l’oggetto MaterialPageRoute, in questo caso un’istanza della classe ListPage() (la pagina elenco creata sopra).

Avviamo l’applicazione, menu Run, Start Debugging (F5 da tastiera) e facciamo un accesso valido (form validato come abbiamo visto nella prima parte della guida):

Come potete vedere, effettuato l’accesso, ci troveremo davanti una pagina vuota chiamata Elenco con un pulsante “+” in basso a destra.

La pagina vuota in realtà è un elenco popolato dalla lista items che però al momento risulta vuoto.

Premendo il pulsante “+” viene stampata all’interno della console la stringa On add pressed!.

Database helper

Ora andremo ad implementare un database locale SQLite sviluppando quello che di fatto è un provider (come abbiamo già visto lo scorso articolo), che chiameremo però DatabaseHelper, quindi helper per convenzione.

Realizzeremo quindi una tabella di questo tipo:

Tabella Persone

CampoTipoNote
IdINTEGERPRIMARY KEY AUTOINCREMENT
NomeTEXTNOT NULL
CognomeTEXTNOT NULL

tradotto in SQL (SQLite):

CREATE TABLE Persone (
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    Nome TEXT NOT NULL,
    Cognome TEXT NOT NULL
)

Successivamente andremo ad aggiungere i metodi per: inserire, eliminare ed ottenere l’elenco dei record salvati in tabella.

Vediamo come…

Per prima cosa dobbiamo installare due nuove librerie, sqflite per la gestione della base dati e path_provider per ottenere percorsi di sistema che possiamo utilizzare (ad esempio per salvare la nostra base dati).

Aggiungiamo quindi al file di progetto pubspec.yaml i due nuovi pacchetti:

sqflite: ^2.0.0+3
path_provider: ^2.0.1

salviamo per far eseguire automaticamente il comando flutter pub get a VSCode.

Al termine, creiamo un nuovo file db_helper.dart all’interno della cartella providers e compiliamolo come segue:

import 'dart:io';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';

class DatabaseHelper {
  final _databaseName = "database.db";
  final _databaseVersion = 1;

  Database? _database;

  Future _initDatabase() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, _databaseName);
    _database = await openDatabase(path,
        version: _databaseVersion, onCreate: _onCreate);
  }

  Future _onCreate(Database db, int version) async {
    await db.execute('''
          CREATE TABLE Persone (
            Id INTEGER PRIMARY KEY AUTOINCREMENT,
            Nome TEXT NOT NULL,
            Cognome TEXT NOT NULL
          )
          ''');
  }
}

Vediamolo nel dettaglio…

final _databaseName = "database.db";
final _databaseVersion = 1;

In questo punto vengono dichiarate ed assegnate due variabili fisse: _databaseName che indica il nome che avrà il file .db creato tramite la libreria sqflite, e _databaseVersion che indica la versione della base dati.

Tramite il metodo _onCreate() che vedremo utilizzato successivamente, è possibile identificare la versione del database, così da gestire il versioning nel caso di aggiornamento della base dati a seguito di un rilascio di una versione successiva dell’applicazione, per esempio.

Future _initDatabase() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, _databaseName);
    _database = await openDatabase(path,
        version: _databaseVersion, onCreate: _onCreate);
}

il metodo asincrono _initDatabase(), genera il percorso finale che avrà la base dati (per fare ciò utilizza il metodo getApplicationDocumentsDirectory della libreria path_provider installata in precedenza), in questo caso la cartella Documenti dell’applicazione, e apre un’istanza database assegnandola alla variabile _database.

Come argomento del metodo openDatabase che si occupa dell’apertura/creazione della base dati, troviamo il metodo _onCreate, che esegue semplicemente la query di creazione della tabella o delle tabelle nel caso ne avessimo più di una.

Mancano ora i metodi per l’interazione con la tabella Persone creata. Prima però abbiamo bisogno di creare un nuovo modello che identifica l’oggetto Persona.

Creiamo quindi un nuovo file persona_db.dart all’interno del percorso models:

class PersonaDB {
  int? id;
  String? nome;
  String? cognome;

  PersonaDB({
    this.id,
    this.nome,
    this.cognome,
  });
}

A differenza degli oggetti creati nel precedente articolo, questo oggetto non necessità n’è di generare un file JSON, n’è di essere generato da una sorgente JSON. I metodi toJson() e fromJson() quindi, non sono presenti.

Creata la classe PersonaDB, aggiungiamo i metodi di interazione all’interno della classe DatabaseHelper del file db_helper.dart:

import 'package:demo_application/models/persona_db.dart';

...
    
Future<int> insertPersona(PersonaDB persona) async {
    if (_database == null) await _initDatabase();
    return await _database!.insert(
      "Persone",
      {
        "Nome": persona.nome,
        "Cognome": persona.cognome,
      },
      conflictAlgorithm: ConflictAlgorithm.fail,
    );
}

Future<List<PersonaDB>> listPersone() async {
    if (_database == null) await _initDatabase();
    List<PersonaDB> result = [];
    List<Map<String, Object?>> rows = await _database!.query("Persone");
    for (var row in rows) {
      result.add(PersonaDB(
        id: row["Id"] as int,
        nome: row["Nome"] as String,
        cognome: row["Cognome"] as String,
      ));
    }
    return result;
}

Future<int> deletePersona(PersonaDB persona) async {
    if (_database == null) await _initDatabase();
    return await _database!.delete(
      "Persone",
      where: "Id = ?",
      whereArgs: [persona.id],
    );
}

Il codice risulta molto esplicito, all’inizio di ogni operazione si verifica se è presente o meno l’istanza _database e nel caso si inizializza.

Successivamente vengono utilizzati i metodi insert, query e delete messi a disposizione dalla libreria sqflite per eseguire le varie operazioni.

In fase di inserimento di un nuovo record non è specificato il campo Id in quanto la chiave ha la proprietà AUTOINCREMENT che ne aumenta automaticamente il progressivo.

Proseguiamo registrando il provider all’interno del file main.dart (seconda parte per la funziona di GetIt):

...
GetIt.I.registerSingleton<DatabaseHelper>(DatabaseHelper());
...

ed associamo il tutto alla nuova pagina Elenco creata.

Elenco interattivo

Torniamo a lavorare sul file list.dart ed aggiungiamo all’interno della classe _ListPage due array fissi nomi e cognomi:

...
final List<String> nomi = [
    "Matteo",
    "Marco",
    "Nicola",
    "Cristian",
    "Elena",
    "Barbara",
];

final List<String> cognomi = [
    "Rossi",
    "Verdi",
    "Gialli",
    "Viola",
];
...

Queste due liste ci serviranno successivamente per inserire valori randomici all’interno della tabella Persone.

Sempre all’interno della classe _ListPage aggiungiamo un nuovo metodo che chiamiamo loadList() importando i necessari riferimenti:

import 'package:get_it/get_it.dart';
import 'package:demo_application/providers/db_helper.dart';

...

Future loadList() async {
    GetIt.I<DatabaseHelper>().listPersone().then((persone) {
      setState(() {
        items.clear();
        for (var persona in persone) {
          items.add(
            ListTile(
              title: Text(persona.id.toString() +
                  ' - ' +
                  persona.nome! +
                  ' ' +
                  persona.cognome!),
              onTap: () {
                GetIt.I<DatabaseHelper>()
                    .deletePersona(persona)
                    .then((value) => loadList());
              },
            ),
          );
        }
      });
    });
}

_ListPage() {
    loadList();
}
...

Il metodo loadList() viene anche invocato all’interno del costruttore, così da caricare la lista alla creazione della pagina.

All’interno del nuovo metodo viene richiamato il metodo listPersone(), dell’istanza DatabaseHelper, per ottenere l’elenco dei record presenti all’interno della tabella Persone.

Elenco di record utilizzato poi per ripopolare l’array items, utilizzato a sua volta per disegnare i vari elementi all’interno del Widget ListView.

Come è possibile vedere, items è un elenco di Widget ListTile che presentano un titolo, in questo caso una stringa contenente: id, nome e cognome di ogni record presente in tabella, ed un evento onTap: () {} (click sull’elemento della lista).

In questo esempio, premendo su un elemento della lista, questo, viene eliminato dalla base dati e la lista viene successivamente ricaricata e ridisegnata:

...
GetIt.I<DatabaseHelper>()
    .deletePersona(persona)
    .then((value) => loadList());
...

Si può notare inoltre che le operazioni di modifica dell’array items vengono fatte all’interno del metodo setState(() {}, questo viene fatto per ridisegnare l’interfaccia (seconda parte per approfondire).

A questo punto non resta che aggiungere un nuovo elemento randomico alla pressione del pulsante “+” presente in basso a destra all’interno della pagina.

Aggiungiamo quindi la seguente operazione all’interno dell’evento onPressed: () {} del pulsante FloatingActionButton:

import 'dart:math';
import 'package:demo_application/models/persona_db.dart';

...
    
GetIt.I<DatabaseHelper>()
    .insertPersona(PersonaDB(
        nome: nomi[random.nextInt(nomi.length)],
        cognome: cognomi[random.nextInt(cognomi.length)],
    ))
    .then((value) => loadList());
...

Il nome ed il cognome vengono selezionati casualmente all’interno delle rispettive liste.

Eseguiamo l’applicazione con F5, effettuiamo l’accesso e premiamo il pulsante “+”:

Alla pressione del pulsante apparirà a schermo un nuovo elemento. Continuamo a premerlo un’altro paio di volte per verificarne il comportamento corretto.

Proviamo ora invece a “tappare” gli elementi della lista:

Come è possibile vedere, gli elementi che vengono premuti verranno eliminati.

Per la prova finale terminiamo l’esecuzione dell’app con Shift+F5 e rieseguiamo il tutto con F5. Accediamo dalla pagina di login… ed ecco qua la nostra lista, esattamente nello stato in cui l’abbiamo lasciata terminando l’applicazione, ad indicare che il database funziona correttamente.

Prossimamente…

Il prossimo sarà l’ultimo articolo di questa serie sullo sviluppo di applicazioni Flutter.

Vedremo con gestire la modalità scura e cambiare il tema della nostra app. Inoltre proveremo a compilarla su un ambiente macOS ed eseguirla su un simulatore iOS per verificarne il funzionamento multipiattaforma.

GitHub

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

Fonti

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

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