Skip to Content
FlutterLezione 5 Accesso Ai Dati5.4 - Persistenza Locale con SQLite

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’interfaccia IExpenseRepository
  • 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

  1. 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
  1. Schermata aggiunta (AddExpenseScreen)
  • TextField per descrizione
  • TextField per importo
  • Dropdown per categoria
  • DatePicker per selezionare la data
  • Bottone “Salva”
  1. 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