Nelle precedenti lezioni abbiamo visto come scrivere file di testo per salvare informazioni all’interno della documents directory di un’app o come salvare dei dati primitivi quali stringhe, interi e booleani tramite le shared preferences.
In questa lezione, vedremo invece come creare e gestire un DB SQLite (di cui abbiamo già parlato qui su HTML.it), uno degli aspetti più importanti durante lo sviluppo di un’app mobile.
I plugin sqflite e path
Per poter lavorare con un DB SQLite, la community di Flutter mette a disposizione degli sviluppatori il plugin sqflite. Il plugin supporta ovviamente sia iOS che Android e offre diverse funzionalità come:
- supporto per le transazioni e operazioni batch;
- gestione automatica della versione durante l'apertura
- helper per la definizione di classiche operazioni CRUD;
- le operazioni di comunicazione col DB sono eseguite in un thread in background sia su iOS sia su Android.
Oltre al plugin sqflite, è necessario utilizzare anche il plugin path, fondamentale per le funzioni offerte per salvare il database SQLite su disco.
Esempio pratico
Come sempre, creiamo un nuovo progetto come descritto nella lezione 6.
Procediamo adesso step by step per creare un’applicazione che ci permetta di gestire un DB SQLite. In particolare, vedremo un semplice esempio per aggiungere, rimuovere, cancellare e selezionare uno o più articoli dal nostro database.
La struttura del nostro progetto sarà quindi la seguente
lib/
│── screens/
│ │── screen1
│ │ │── screen1.dart
│── theme/
│ │── style.dart
│── models/
│ │── article.dart
│── repositories/
│ │── dbhelper.dart
│ │── article_repository.dart
│── routes.dart
In questa lezione, non vedremo i dettagli implementativi dell’interfaccia utente, lasciando al lettore la libertà di implementare una propria soluzione o di prendere spunto da quella offerta nel progetto rilasciato su GitHub.
Installazione dei plugin
Apriamo il file pubspec.yaml e inseriamo sotto la voce dependencies i nomi dei plugin di interesse come segue
dependencies:
# . . .
sqflite: ^1.3.0
path:
Eseguiamo dal nostro terminale il comando
flutter pub get
per installare il plugin.
Definizione della classe Article
Definiamo la classe Article che ci permetterà di descrivere alcune semplici caratteristiche di un articolo che sono:
- il titolo;
- l’autore;
- un id univoco che lo identifichi.
Una semplice implementazione può essere ad esempio la seguente.
class Article {
@required
final int id;
@required
String title;
@required
String author;
Article(this.id, this.title, this.author);
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'author': author,
};
}
Article.fromMapObject(Map<String, dynamic> authorMap)
: id = authorMap['id'],
title = authorMap['title'],
author = authorMap['author'];
@override
String toString() {
return 'Article{id: $id, title: $title, author: $author}';
}
}
Oltre alla definizione della classe abbiamo aggiunto altri tre metodi di utilità:
Metodo | Descrizione |
---|---|
toMap |
permette di trasformare l’oggetto corrente in una Map |
fromMapToObject |
permette di trasformare una Map composta dalle caratteristiche della classe in un oggetto di quella classe |
toString |
semplice override del metodo toString per ottenere facilmente una descrizione dell’oggetto |
Definizione dello strato repository
Definiamo ora le classi necessarie per la creazione del nostro database e per interagire con questo.
Partiamo dalla creazione e gestione del database e definiamo il file dbhelper.dart che conterrà:
- una variabile db di tipo
Database
offerta da sqflite che rappresenta il database da interrogare; - la classe
DBHelper
.
Lo scopo di questa classe è quello di:
- definire la struttura del database creando le tabelle di interesse;
- esporre un metodo per inizializzare il db.
Per fare ciò dobbiamo creare i seguenti metodi asincroni
Metodo | Descrizione |
---|---|
createArticleTable |
responsabile della creazione della tabella Article tramite semplice query CREATE di SQLite |
getDatabasePath |
definisce il percorso al file rappresentante il DB recuperato tramite il metodo getDatabasesPath di sqflite combinato tramite join con il nome desiderato per il database. Se non esistono, crea il file e le relative sottocartelle |
initDatabase |
esegue l’inizializzazione del db invocando il metodo openDatabase di sqflite e creando la tabella di interesse tramite la callback onCreate . Questo metodo è importante in quanto permette anche di effettuare gli aggiornamenti di versione |
Si riporta di seguito una semplice implementazione della classe DBHelper:
Database db;
class DBHelper {
static const dbName = 'htmlit';
static const articlesTable = 'articles';
static const id = 'id';
static const title = 'title';
static const author = 'author';
Future<void> createArticleTable(Database db) async {
final todoSql = '''CREATE TABLE $articlesTable (
$id INTEGER PRIMARY KEY,
$title TEXT,
$author TEXT)''';
await db.execute(todoSql);
}
Future<String> getDatabasePath(String dbName) async {
final databasePath = await getDatabasesPath();
final path = join(databasePath, dbName);
if (!await Directory(dirname(path)).exists()) {
await Directory(dirname(path)).create(recursive: true);
}
return path;
}
Future<void> initDatabase() async {
final path = await getDatabasePath(dbName);
db = await openDatabase(path, version: 1, onCreate: onCreate);
print(db);
}
Future<void> onCreate(Database db, int version) async {
await createArticleTable(db);
}
}
Passiamo adesso alla definizione dello strato di repository e di interrogazione dei nostri dati e creiamo il file article_repository.dart che conterrà l’omonima classe ArticleRepository
.
Vediamo un semplice esempio di come si può inserire un nuovo oggetto nel DB tramite una query di INSERT
.
class ArticleRepository {
static Future<void> addArticle(Article article) async {
final sql = '''INSERT INTO ${DBHelper.articlesTable}(
${DBHelper.id},
${DBHelper.title},
${DBHelper.author})
VALUES (?,?,?)
''';
List<dynamic> params = [article.id, article.title, article.author];
await db.rawInsert(sql, params);
}
}
In questo caso abbiamo:
- assato al nostro metodo asincrono un oggetto
Article
; - definito la query SQL da eseguire;
- definita la lista di parametri nell’ordine desiderato dalla query SQL;
- invocato il metodo
rawInsert
passando come parametro la query sql e i parametri.
Il metodo rawInsert
si occuperà di costruire la query ed eseguirla.
Questo approccio è però eccessivamente verboso e può essere facilmente sostituito da una versione più snella offerta dal metodo insert
di sqflite. Vediamo come cambia la precedente implementazione.
static Future<void> addArticle(Article article) async {
await db.insert(DBHelper.articlesTable, article.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
}
In questo caso il metodo ha richiesto come input:
- la tabella su cui eseguire la query;
- le informazioni da passare in formato chiave-valore e ottenibili tramite il metodo
Article.toMap
; - l’algoritmo da utilizzare in caso di conflitti.
Oltre al metodo insert
, la libreria offre anche altri metodi quali delete
, update
e query
per cancellare, aggiornare ed eseguire query come la selezione di tutti gli articoli presenti nella tabella.
Lasciamo quindi al lettore l’implementazione di questi metodi ricordando che potrà trovare una possibile implementazione nel codice rilasciato per questa lezione.
Inizializzazione del DB
Ora che abbiamo definito lo strato repository possiamo inizializzare finalmente il nostro DB all’avvio dell’app.
All’interno del file main.dart sostituiamo la definizione del metodo main() con la seguente implementazione.
void main() async {
await DBHelper().initDatabase();
runApp(MyApp());
}
In questo modo, all’avvio dell’applicazione il DB verrà inizializzato e potrà essere interrogato in modo semplice e diretto.
Immaginando di aver definito un’interfaccia composta da una form per creare nuovi articoli e da una lista che si aggiorna automaticamente ad ogni nuova interrogazione al DB, eseguiamo la nostra applicazione ottenendo il risultato in figura.
Come possiamo vedere, in poche e semplici righe di codice abbiamo integrato all’interno della nostra applicazione un DB SQLite per gestire le informazioni riguardanti un articolo.
Il codice di questa lezione con le relative soluzioni su come implementare i metodi CRUD e l’interfaccia utente sono disponibili su GitHub.