Persistenza Locale con SQLite
1. Introduzione
SQLite è un database relazionale self-contained, serverless e zero-configuration integrato direttamente nel dispositivo. In Flutter, è spesso utilizzato per la persistenza locale dei dati attraverso plugin come sqflite
.
Caratteristiche principali
- Archiviazione completa dei dati in locale
- Supporta transazioni, join, trigger e foreign key
- Accesso asincrono e performante
- File di database unico
.db
, compatibile su Android, iOS, macOS, Linux e Windows
Quando usarlo
SQLite è utile per:
- App che devono funzionare completamente offline
- Cache persistente di dati sincronizzati da API remote
- Gestione di dati strutturati complessi (tabelle, relazioni, vincoli)
- Situazioni dove SharedPreferences non è sufficiente
Esempi di utilizzo pratico
- Liste di cose da fare (TODO), note, ricette
- Dati di esercizio o alimentazione salvati offline
- Applicazioni mediche, giornaliere, o di campo senza connettività
Limiti
- Non adatto per dati in tempo reale o per sincronizzazione multiutente avanzata
- Richiede scrittura di query SQL (a meno che non si usi un ORM come Drift)
2. Setup
Aggiunta al pubspec.yaml
dependencies:
sqflite: ^2.3.0
path: ^1.8.0
Import nei file Dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
3. Inizializzazione del Database e gestione versioni
late Database db;
Future<void> initDb() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'todo.db');
db = await openDatabase(
path,
version: 2, // Incrementa se apporti modifiche allo schema
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE todos(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
completed INTEGER
)
''');
},
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
// Esempio: aggiunta di una nuova colonna 'priority'
await db.execute('ALTER TABLE todos ADD COLUMN priority INTEGER DEFAULT 0');
}
},
);
}
Esegui initDb()
all’avvio, idealmente nel main()
.
Ogni volta che modifichi lo schema del database (es. aggiungi colonne, tabelle), incrementa la versione nel openDatabase(...)
e gestisci le modifiche in onUpgrade()
.
Questo è essenziale per supportare l’evoluzione dell’app mantenendo i dati degli utenti già esistenti.
4. Operazioni CRUD
Inserimento
Future<void> insertTodo(Todo todo) async {
await db.insert(
'todos',
todo.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
Lettura
Future<List<Todo>> getTodos() async {
final List<Map<String, dynamic>> maps = await db.query('todos');
return List.generate(maps.length, (i) => Todo.fromMap(maps[i]));
}
Aggiornamento
Future<void> updateTodo(Todo todo) async {
await db.update(
'todos',
todo.toMap(),
where: 'id = ?',
whereArgs: [todo.id],
);
}
Eliminazione
Future<void> deleteTodo(int id) async {
await db.delete(
'todos',
where: 'id = ?',
whereArgs: [id],
);
}
5. Modello Todo
class Todo {
final int? id;
final String title;
final bool completed;
Todo({this.id, required this.title, this.completed = false});
Map<String, dynamic> toMap() => {
'id': id,
'title': title,
'completed': completed ? 1 : 0,
};
factory Todo.fromMap(Map<String, dynamic> map) => Todo(
id: map['id'],
title: map['title'],
completed: map['completed'] == 1,
);
}
6. UI: Visualizzare i Todo
class TodoListPage extends StatefulWidget {
const TodoListPage({super.key});
@override
State<TodoListPage> createState() => _TodoListPageState();
}
class _TodoListPageState extends State<TodoListPage> {
List<Todo> todos = [];
@override
void initState() {
super.initState();
initDb().then((_) => refreshList());
}
Future<void> refreshList() async {
final result = await getTodos();
setState(() => todos = result);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('SQLite Todo')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return CheckboxListTile(
value: todo.completed,
title: Text(todo.title),
onChanged: (val) async {
final updated = Todo(id: todo.id, title: todo.title, completed: val ?? false);
await updateTodo(updated);
await refreshList();
},
secondary: IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
await deleteTodo(todo.id!);
await refreshList();
},
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final newTodo = Todo(title: 'Nuovo todo');
await insertTodo(newTodo);
await refreshList();
},
child: const Icon(Icons.add),
),
);
}
}
7. Buone pratiche
- Inizializza il database prima di usare qualsiasi query
- Usa try/catch per proteggere le operazioni critiche
- Evita operazioni pesanti nel
build()
- Non usare tipi non supportati direttamente (es. DateTime → salva come stringa o timestamp)
Esercizio: App Gestione Spese con Repository + BLoC + SQLite
In questo esercizio è necessario una mini-app Flutter per gestire le spese personali, salvando i dati in locale tramite SQLite. Verranno applicati i concetti di Repository Pattern, gestione dello stato con flutter_bloc
, uso di più schermate e widget dinamici.
Obiettivi dell’esercizio
- Implementare un repository
SqliteExpenseRepository
conforme all’interfacciaIExpenseRepository
- Collegare il repository a un
Bloc
- Visualizzare e aggiungere
- Calcolare il totale spese
- Utilizzare
DatePicker
,Dropdown
,ListView
,TextField
,Dismissible
Struttura del progetto
/lib
/models
expense.dart
/repositories
expense_repository_interface.dart
expense_sqlite_repository.dart
/blocs
/expense
expense_state.dart
expense_event.dart
expense_bloc.dart
/screens
expense_list_screen.dart
add_expense_screen.dart
main.dart
Requisiti funzionali
- Schermata principale (ExpenseListScreen)
- Visualizza l’elenco delle spese con:
- Descrizione
- Importo
- Categoria (es. cibo, trasporti, altro)
- Data
- Mostra il totale spese correnti
- Swipe per eliminare
- Schermata aggiunta (AddExpenseScreen)
TextField
per descrizioneTextField
per importoDropdown
per categoriaDatePicker
per selezionare la data- Bottone “Salva”
- Persistenza locale
- Salvataggio nel DB SQLite
- Recupero, inserimento, cancellazione tramite repository
Struttura SQL consigliata
La tabella per le spese può essere creata così:
Nel codice Dart:
await db.execute('''
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT,
amount REAL,
category TEXT,
date TEXT
)
''');
Eventi Bloc
LoadExpenses
AddExpense
DeleteExpense
Extra bonus features:
- Aggiunge ordinamento per data
- Mostra un
DropdownButton
per filtrare per categoria - Pagina statistiche totali spese divise per categorie