flutter_data_persistence
Flutter - Data Persistence (SQLite & Cloud Firestore)
Section titled “Flutter - Data Persistence (SQLite & Cloud Firestore)”Storing and retrieving data is crucial for most applications. Flutter offers various ways to persist data locally and remotely. This chapter explores two popular options: SQLite for local relational data and Cloud Firestore for cloud-based NoSQL data.
Local Persistence with SQLite using sqflite
Section titled “Local Persistence with SQLite using sqflite”sqflite is a popular Flutter plugin that provides access to the native SQLite database implementation on both Android and iOS. It allows you to perform standard SQL operations.
Key functionalities provided by sqflite:
- Creating or opening a database (
openDatabase). - Executing raw SQL statements (
execute,rawQuery,rawInsert,rawUpdate,rawDelete). - Convenience methods for CRUD operations (
query,insert,update,delete). - Batch operations for efficiency.
- Database versioning and migration support during
onCreate,onUpgrade,onDowngrade.
Let’s build a simple product storage application using sqflite.
-
Create a new Flutter project:
flutter create product_sqflite_app -
Add dependencies to
pubspec.yaml. Use the latest versions from pub.dev: -
Run
flutter pub getto install the packages. -
Create a
Productmodel class (e.g., inlib/product.dart) withtoMapandfromMapmethods for serialization:
Database Helper Class
Section titled “Database Helper Class”Create a singleton class (e.g., lib/database_helper.dart) to manage database operations:
import 'dart:async';import 'package:path/path.dart';import 'package:sqflite/sqflite.dart';import 'product.dart'; // Import your Product model
class DatabaseHelper { static final DatabaseHelper _instance = DatabaseHelper._internal(); factory DatabaseHelper() => _instance;
static Database? _database;
DatabaseHelper._internal();
Future<Database> get database async { if (_database != null) return _database!; _database = await _initDb(); return _database!; }
Future<Database> _initDb() async { String path = join(await getDatabasesPath(), 'product_database.db'); return await openDatabase( path, version: 1, onCreate: _onCreate, ); }
Future<void> _onCreate(Database db, int version) async { await db.execute(''' CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT NOT NULL, price REAL NOT NULL, image TEXT NOT NULL ) '''); // Optionally insert initial data await _insertInitialData(db); }
Future<void> _insertInitialData(Database db) async { // Using batch for efficiency if inserting multiple items Batch batch = db.batch(); batch.insert('products', {'name': 'Laptop', 'description': 'High-performance laptop', 'price': 1200.0, 'image': 'laptop.png'}); batch.insert('products', {'name': 'Smartphone', 'description': 'Latest model smartphone', 'price': 800.0, 'image': 'phone.png'}); await batch.commit(noResult: true); }
// Insert a Product Future<int> insertProduct(Product product) async { final db = await database; // Use toMap, excluding the id since it's auto-incremented Map<String, dynamic> row = product.toMap(); row.remove('id'); // Let SQLite handle the ID return await db.insert('products', row, conflictAlgorithm: ConflictAlgorithm.replace); }
// Get all Products Future<List<Product>> getAllProducts() async { final db = await database; final List<Map<String, dynamic>> maps = await db.query('products', orderBy: 'name ASC');
return List.generate(maps.length, (i) { return Product.fromMap(maps[i]); }); }
// Get a single Product by ID Future<Product?> getProductById(int id) async { final db = await database; List<Map<String, dynamic>> maps = await db.query( 'products', where: 'id = ?', whereArgs: [id], ); if (maps.isNotEmpty) { return Product.fromMap(maps.first); } else { return null; } }
// Update a Product Future<int> updateProduct(Product product) async { final db = await database; return await db.update( 'products', product.toMap(), where: 'id = ?', whereArgs: [product.id], ); }
// Delete a Product Future<int> deleteProduct(int id) async { final db = await database; return await db.delete( 'products', where: 'id = ?', whereArgs: [id], ); }
// Close the database (optional, typically managed by sqflite) Future<void> close() async { final db = await database; db.close(); _database = null; // Reset the static variable }}Using the Database Helper in UI
Section titled “Using the Database Helper in UI”In your UI code (e.g., within a StatefulWidget or using a state management solution), you can interact with the database:
import 'package:flutter/material.dart';import 'database_helper.dart';import 'product.dart';
// Example usage within a StatefulWidget's Stateclass ProductListScreen extends StatefulWidget { @override _ProductListScreenState createState() => _ProductListScreenState();}
class _ProductListScreenState extends State<ProductListScreen> { late Future<List<Product>> _productsFuture; final dbHelper = DatabaseHelper();
@override void initState() { super.initState(); _refreshProductList(); }
void _refreshProductList() { setState(() { _productsFuture = dbHelper.getAllProducts(); }); }
void _addProduct() async { // Example: Show a dialog or navigate to a form to get product details Product newProduct = Product( name: 'New Gadget', description: 'Exciting new gadget', price: 99.99, image: 'gadget.png'); await dbHelper.insertProduct(newProduct); _refreshProductList(); // Update the list }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('SQLite Products')), body: FutureBuilder<List<Product>>( future: _productsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { return Center(child: Text('Error: ${snapshot.error}')); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return Center(child: Text('No products found.')); } else { final products = snapshot.data!; return ListView.builder( itemCount: products.length, itemBuilder: (context, index) { final product = products[index]; return ListTile( // Assuming images are in assets/images/ // leading: Image.asset('assets/images/${product.image}', width: 50, height: 50, fit: BoxFit.cover), title: Text(product.name), subtitle: Text(product.description), trailing: Text('\$${product.price.toStringAsFixed(2)}'), onTap: () async { // Example: Delete on tap if (product.id != null) { await dbHelper.deleteProduct(product.id!); _refreshProductList(); } }, ); }, ); } }, ), floatingActionButton: FloatingActionButton( onPressed: _addProduct, child: Icon(Icons.add), ), ); }}This example demonstrates basic setup and CRUD operations. Real applications would involve proper error handling, user input forms, and potentially more complex queries.
Cloud Persistence with Cloud Firestore
Section titled “Cloud Persistence with Cloud Firestore”Cloud Firestore is a flexible, scalable NoSQL cloud database from Firebase and Google Cloud. It keeps your data in sync across client apps through realtime listeners and offers offline support.
Key Concepts:
- Documents: Units of storage, similar to JSON objects.
- Collections: Containers for documents.
- Realtime Updates: Use
snapshots()streams to listen for data changes. - Offline Support: Data is cached locally for offline access.
- Scalability: Designed for high scalability and performance.
-
Create a Firebase Project: Go to the Firebase Console (firebase.google.com), create a project, and add an Android and/or iOS app to it.
-
Download Configuration Files: Download
google-services.json(Android) andGoogleService-Info.plist(iOS) and place them in the correct directories within your Flutter project (android/app/andios/Runner/). -
Add Firebase Core and Firestore Dependencies: Add the following to your
pubspec.yaml(use latest versions): -
Configure Native Projects: Follow the platform-specific setup instructions provided by the Firebase documentation (adding plugins to
build.gradlefiles for Android, potentially modifyingAppDelegatefor iOS, enabling multidex if needed). This setup can change slightly with Flutter/Firebase updates, so always refer to the official docs. -
Initialize Firebase: In your
main.dart, initialize Firebase before running the app:
Firestore Operations
Section titled “Firestore Operations”Use the cloud_firestore package to interact with Firestore. Assume you have a products collection.
import 'package:cloud_firestore/cloud_firestore.dart';import 'product.dart'; // Your Product model (needs fromMap/toMap or equivalent)
class FirestoreService { final FirebaseFirestore _db = FirebaseFirestore.instance; final CollectionReference _productsCollection = FirebaseFirestore.instance.collection('products');
// Add a new Product (Firestore generates the ID) Future<DocumentReference> addProduct(Product product) { // Assuming Product.toMap() exists and returns Map<String, dynamic> return _productsCollection.add(product.toMap()..remove('id')); // Firestore handles ID }
// Get a stream of all products (realtime updates) Stream<List<Product>> getProductsStream() { return _productsCollection.snapshots().map((snapshot) { return snapshot.docs.map((doc) { // Assuming Product.fromMap exists // Need to add the document ID to the map before creating the object final data = doc.data() as Map<String, dynamic>; data['id_firestore'] = doc.id; // Store Firestore ID if needed separately return Product.fromMap(data); // Adapt fromMap if necessary }).toList(); }); }
// Get a single product (one-time read) Future<Product?> getProduct(String docId) async { DocumentSnapshot doc = await _productsCollection.doc(docId).get(); if (doc.exists) { final data = doc.data() as Map<String, dynamic>; data['id_firestore'] = doc.id; return Product.fromMap(data); } return null; }
// Update a Product Future<void> updateProduct(String docId, Product product) { return _productsCollection.doc(docId).update(product.toMap()); }
// Delete a Product Future<void> deleteProduct(String docId) { return _productsCollection.doc(docId).delete(); }}Using Firestore Streams in UI
Section titled “Using Firestore Streams in UI”Use StreamBuilder to listen to Firestore streams and update the UI automatically.
import 'package:flutter/material.dart';import 'package:cloud_firestore/cloud_firestore.dart';import 'firestore_service.dart'; // Your Firestore serviceimport 'product.dart';
class FirestoreProductListScreen extends StatelessWidget { final FirestoreService _firestoreService = FirestoreService();
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Firestore Products (Realtime)')), body: StreamBuilder<List<Product>>( stream: _firestoreService.getProductsStream(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { print('Firestore Error: ${snapshot.error}'); // Log the error return Center(child: Text('Error loading products. See console for details.')); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return Center(child: Text('No products found in Firestore.')); } else { final products = snapshot.data!; return ListView.builder( itemCount: products.length, itemBuilder: (context, index) { final product = products[index]; return ListTile( title: Text(product.name), subtitle: Text(product.description), trailing: Text('\$${product.price.toStringAsFixed(2)}'), // Add onTap/onLongPress for update/delete actions using product.id_firestore ); }, ); } }, ), floatingActionButton: FloatingActionButton( onPressed: () { // Example: Add a product _firestoreService.addProduct(Product(name: 'Cloud Widget', description: 'Syncs magically', price: 199.99, image: 'cloud.png')); }, child: Icon(Icons.add), ), ); }}Other Persistence Options
Section titled “Other Persistence Options”Besides sqflite and cloud_firestore, consider these:
shared_preferences: For storing simple key-value pairs (settings, flags). Very lightweight.hive: A fast, native key-value database written in Dart. Good for more complex local data thanshared_preferences.drift(formerly Moor): A reactive persistence library built on top ofsqfliteorsql.js, offering compile-time safety and generated code for queries.isar: A very fast, cross-platform database focused on performance and ease of use.
Choosing the right persistence method depends on your data’s complexity, whether you need offline access, realtime updates, or relational query capabilities.
Further Reading: Check the official documentation for sqflite, cloud_firestore, and other persistence packages on pub.dev for more detailed examples and advanced features.