Skip to content

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 get to install the packages.

  • Create a Product model class (e.g., in lib/product.dart) with toMap and fromMap methods for serialization:

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
}
}

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 State
class 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 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) and GoogleService-Info.plist (iOS) and place them in the correct directories within your Flutter project (android/app/ and ios/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.gradle files for Android, potentially modifying AppDelegate for 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:

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();
}
}

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 service
import '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),
),
);
}
}

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 than shared_preferences.
  • drift (formerly Moor): A reactive persistence library built on top of sqflite or sql.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.