SQLite (sqflite)
sqflite is a lightweight and high-performance SQLite database library for Flutter, positioned as the "definitive solution for local data persistence on iOS, Android, and macOS." As the de facto standard SQLite implementation in the Flutter ecosystem, it provides a comprehensive solution for structured data management in mobile applications, balancing native performance and type safety, specifically tailored for mobile app development.
Library
SQLite (sqflite)
Overview
sqflite is a lightweight and high-performance SQLite database library for Flutter, positioned as the "definitive solution for local data persistence on iOS, Android, and macOS." As the de facto standard SQLite implementation in the Flutter ecosystem, it provides a comprehensive solution for structured data management in mobile applications, balancing native performance and type safety, specifically tailored for mobile app development.
Details
sqflite 2025 version fully integrates with Flutter 3.0+ latest features, delivering a consistent SQLite database experience across iOS, Android, macOS, and Web (experimental). Through direct integration with the native SQLite engine, it maintains high performance and memory efficiency while providing modern asynchronous programming patterns through Dart Future-based APIs and async/await support. It comprehensively supports database schema management, migrations, batch processing, and transaction control, suitable for enterprise-level mobile application development.
Key Features
- Multi-platform Support: Unified API across iOS, Android, macOS, and Web (experimental)
- High-performance Native Implementation: Optimized performance through direct SQLite engine integration
- Asynchronous Processing Support: Modern API design based on Future/async-await
- Comprehensive CRUD Features: Complete CRUD operations and SQL statement execution support
- Schema Management: Database versioning and migration capabilities
- Transaction Control: Safe data operations with guaranteed ACID properties
Pros and Cons
Pros
- Standard position in Flutter ecosystem with abundant learning resources
- High-speed database operations through native performance
- Lightweight implementation with minimal impact on app size
- Safe transaction processing with robust ACID properties
- Low learning cost and high development efficiency through intuitive Dart API
- Rich community support and third-party integrations
Cons
- Limited advanced relational features due to SQLite constraints
- Local storage only; cloud sync functionality requires separate implementation
- Performance constraints with large datasets
- Web implementation is experimental with existing limitations
- Readability challenges with raw SQL writing for complex query construction
- Unsuitable for NoSQL data models
Reference Pages
Code Examples
Project Setup and Dependencies
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
sqflite: ^2.4.2
path: ^1.8.3
dev_dependencies:
flutter_test:
sdk: flutter
# Dart imports
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
Database Initialization and Setup
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static const _databaseName = "app_database.db";
static const _databaseVersion = 1;
// Table definitions
static const String tableUsers = 'users';
static const String tablePosts = 'posts';
// User table columns
static const String columnUserId = 'id';
static const String columnUserName = 'name';
static const String columnUserEmail = 'email';
static const String columnUserAge = 'age';
static const String columnUserCreatedAt = 'created_at';
// Post table columns
static const String columnPostId = 'id';
static const String columnPostTitle = 'title';
static const String columnPostContent = 'content';
static const String columnPostUserId = 'user_id';
static const String columnPostCreatedAt = 'created_at';
static Database? _database;
// Singleton pattern for database instance management
static Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
// Database initialization
static Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), _databaseName);
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
// Table creation
static Future<void> _onCreate(Database db, int version) async {
// Create users table
await db.execute('''
CREATE TABLE $tableUsers (
$columnUserId INTEGER PRIMARY KEY AUTOINCREMENT,
$columnUserName TEXT NOT NULL,
$columnUserEmail TEXT UNIQUE NOT NULL,
$columnUserAge INTEGER,
$columnUserCreatedAt TEXT NOT NULL
)
''');
// Create posts table
await db.execute('''
CREATE TABLE $tablePosts (
$columnPostId INTEGER PRIMARY KEY AUTOINCREMENT,
$columnPostTitle TEXT NOT NULL,
$columnPostContent TEXT NOT NULL,
$columnPostUserId INTEGER NOT NULL,
$columnPostCreatedAt TEXT NOT NULL,
FOREIGN KEY ($columnPostUserId) REFERENCES $tableUsers ($columnUserId)
)
''');
// Create indexes
await db.execute('''
CREATE INDEX idx_posts_user_id ON $tablePosts($columnPostUserId)
''');
await db.execute('''
CREATE INDEX idx_users_email ON $tableUsers($columnUserEmail)
''');
}
// Database upgrade
static Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Version 2 migration example
await db.execute('''
ALTER TABLE $tableUsers ADD COLUMN profile_image TEXT
''');
}
if (oldVersion < 3) {
// Version 3 migration example
await db.execute('''
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT
)
''');
}
}
// Close database connection
static Future<void> closeDatabase() async {
final db = _database;
if (db != null) {
await db.close();
_database = null;
}
}
}
Model Class Definition
// User model
class User {
final int? id;
final String name;
final String email;
final int? age;
final DateTime createdAt;
User({
this.id,
required this.name,
required this.email,
this.age,
required this.createdAt,
});
// Map conversion from database
factory User.fromMap(Map<String, dynamic> map) {
return User(
id: map[DatabaseHelper.columnUserId],
name: map[DatabaseHelper.columnUserName],
email: map[DatabaseHelper.columnUserEmail],
age: map[DatabaseHelper.columnUserAge],
createdAt: DateTime.parse(map[DatabaseHelper.columnUserCreatedAt]),
);
}
// Map conversion to database
Map<String, dynamic> toMap() {
return {
DatabaseHelper.columnUserId: id,
DatabaseHelper.columnUserName: name,
DatabaseHelper.columnUserEmail: email,
DatabaseHelper.columnUserAge: age,
DatabaseHelper.columnUserCreatedAt: createdAt.toIso8601String(),
};
}
// Debug string representation
@override
String toString() {
return 'User{id: $id, name: $name, email: $email, age: $age, createdAt: $createdAt}';
}
// Copy with method
User copyWith({
int? id,
String? name,
String? email,
int? age,
DateTime? createdAt,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
age: age ?? this.age,
createdAt: createdAt ?? this.createdAt,
);
}
}
// Post model
class Post {
final int? id;
final String title;
final String content;
final int userId;
final DateTime createdAt;
Post({
this.id,
required this.title,
required this.content,
required this.userId,
required this.createdAt,
});
factory Post.fromMap(Map<String, dynamic> map) {
return Post(
id: map[DatabaseHelper.columnPostId],
title: map[DatabaseHelper.columnPostTitle],
content: map[DatabaseHelper.columnPostContent],
userId: map[DatabaseHelper.columnPostUserId],
createdAt: DateTime.parse(map[DatabaseHelper.columnPostCreatedAt]),
);
}
Map<String, dynamic> toMap() {
return {
DatabaseHelper.columnPostId: id,
DatabaseHelper.columnPostTitle: title,
DatabaseHelper.columnPostContent: content,
DatabaseHelper.columnPostUserId: userId,
DatabaseHelper.columnPostCreatedAt: createdAt.toIso8601String(),
};
}
@override
String toString() {
return 'Post{id: $id, title: $title, content: $content, userId: $userId, createdAt: $createdAt}';
}
}
// User with posts combined model
class UserWithPosts {
final User user;
final List<Post> posts;
UserWithPosts({
required this.user,
required this.posts,
});
@override
String toString() {
return 'UserWithPosts{user: $user, posts: ${posts.length} posts}';
}
}
Basic CRUD Operations
class UserRepository {
// Create user
static Future<int> insertUser(User user) async {
final Database db = await DatabaseHelper.database;
try {
final Map<String, dynamic> userMap = user.toMap();
userMap.remove(DatabaseHelper.columnUserId); // Remove ID for auto-increment
int userId = await db.insert(
DatabaseHelper.tableUsers,
userMap,
conflictAlgorithm: ConflictAlgorithm.abort,
);
print('User created: ID $userId');
return userId;
} catch (e) {
print('User creation error: $e');
rethrow;
}
}
// Get all users
static Future<List<User>> getAllUsers() async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tableUsers,
orderBy: '${DatabaseHelper.columnUserCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return User.fromMap(maps[i]);
});
}
// Get user by ID
static Future<User?> getUserById(int id) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return User.fromMap(maps.first);
}
return null;
}
// Search user by email
static Future<User?> getUserByEmail(String email) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserEmail} = ?',
whereArgs: [email],
);
if (maps.isNotEmpty) {
return User.fromMap(maps.first);
}
return null;
}
// Update user information
static Future<bool> updateUser(User user) async {
final Database db = await DatabaseHelper.database;
try {
int count = await db.update(
DatabaseHelper.tableUsers,
user.toMap(),
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [user.id],
);
print('User updated: $count records');
return count > 0;
} catch (e) {
print('User update error: $e');
return false;
}
}
// Delete user
static Future<bool> deleteUser(int id) async {
final Database db = await DatabaseHelper.database;
try {
// Delete related posts (cascade delete)
await db.delete(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostUserId} = ?',
whereArgs: [id],
);
int count = await db.delete(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [id],
);
print('User deleted: $count records');
return count > 0;
} catch (e) {
print('User deletion error: $e');
return false;
}
}
// Search users by age range
static Future<List<User>> getUsersByAgeRange(int minAge, int maxAge) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserAge} BETWEEN ? AND ?',
whereArgs: [minAge, maxAge],
orderBy: '${DatabaseHelper.columnUserAge} ASC',
);
return List.generate(maps.length, (i) {
return User.fromMap(maps[i]);
});
}
// Count users
static Future<int> getUserCount() async {
final Database db = await DatabaseHelper.database;
var result = await db.rawQuery('SELECT COUNT(*) FROM ${DatabaseHelper.tableUsers}');
return Sqflite.firstIntValue(result) ?? 0;
}
}
class PostRepository {
// Create post
static Future<int> insertPost(Post post) async {
final Database db = await DatabaseHelper.database;
try {
final Map<String, dynamic> postMap = post.toMap();
postMap.remove(DatabaseHelper.columnPostId);
int postId = await db.insert(
DatabaseHelper.tablePosts,
postMap,
conflictAlgorithm: ConflictAlgorithm.abort,
);
print('Post created: ID $postId');
return postId;
} catch (e) {
print('Post creation error: $e');
rethrow;
}
}
// Get posts by user ID
static Future<List<Post>> getPostsByUserId(int userId) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostUserId} = ?',
whereArgs: [userId],
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
// Get all posts
static Future<List<Post>> getAllPosts() async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
// Delete post
static Future<bool> deletePost(int id) async {
final Database db = await DatabaseHelper.database;
try {
int count = await db.delete(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostId} = ?',
whereArgs: [id],
);
return count > 0;
} catch (e) {
print('Post deletion error: $e');
return false;
}
}
}
Advanced Queries and JOIN Operations
class AdvancedQueries {
// JOIN users and posts
static Future<List<UserWithPosts>> getUsersWithPosts() async {
final Database db = await DatabaseHelper.database;
// Get users
final users = await UserRepository.getAllUsers();
List<UserWithPosts> result = [];
for (User user in users) {
final posts = await PostRepository.getPostsByUserId(user.id!);
result.add(UserWithPosts(user: user, posts: posts));
}
return result;
}
// Complex JOIN query with raw SQL
static Future<List<Map<String, dynamic>>> getUserPostStats() async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> result = await db.rawQuery('''
SELECT
u.${DatabaseHelper.columnUserId} as user_id,
u.${DatabaseHelper.columnUserName} as user_name,
u.${DatabaseHelper.columnUserEmail} as user_email,
COUNT(p.${DatabaseHelper.columnPostId}) as post_count,
MAX(p.${DatabaseHelper.columnPostCreatedAt}) as latest_post_date
FROM ${DatabaseHelper.tableUsers} u
LEFT JOIN ${DatabaseHelper.tablePosts} p
ON u.${DatabaseHelper.columnUserId} = p.${DatabaseHelper.columnPostUserId}
GROUP BY u.${DatabaseHelper.columnUserId}, u.${DatabaseHelper.columnUserName}, u.${DatabaseHelper.columnUserEmail}
ORDER BY post_count DESC
''');
return result;
}
// Search posts by date range
static Future<List<Post>> getPostsByDateRange(DateTime startDate, DateTime endDate) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostCreatedAt} BETWEEN ? AND ?',
whereArgs: [startDate.toIso8601String(), endDate.toIso8601String()],
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
// Full-text search (using LIKE)
static Future<List<Post>> searchPosts(String keyword) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
where: '${DatabaseHelper.columnPostTitle} LIKE ? OR ${DatabaseHelper.columnPostContent} LIKE ?',
whereArgs: ['%$keyword%', '%$keyword%'],
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
// Pagination support
static Future<List<Post>> getPostsPaginated(int page, int pageSize) async {
final Database db = await DatabaseHelper.database;
final List<Map<String, dynamic>> maps = await db.query(
DatabaseHelper.tablePosts,
orderBy: '${DatabaseHelper.columnPostCreatedAt} DESC',
limit: pageSize,
offset: page * pageSize,
);
return List.generate(maps.length, (i) {
return Post.fromMap(maps[i]);
});
}
}
Transaction Processing and Batch Operations
class TransactionOperations {
// Transaction usage example
static Future<void> createUserWithPosts(User user, List<Post> posts) async {
final Database db = await DatabaseHelper.database;
await db.transaction((txn) async {
try {
// Create user
final Map<String, dynamic> userMap = user.toMap();
userMap.remove(DatabaseHelper.columnUserId);
int userId = await txn.insert(
DatabaseHelper.tableUsers,
userMap,
);
// Create posts
for (Post post in posts) {
final Map<String, dynamic> postMap = post.toMap();
postMap.remove(DatabaseHelper.columnPostId);
postMap[DatabaseHelper.columnPostUserId] = userId;
await txn.insert(
DatabaseHelper.tablePosts,
postMap,
);
}
print('Created user and ${posts.length} posts');
} catch (e) {
print('Transaction error: $e');
rethrow; // Rollback will be executed
}
});
}
// Batch operations
static Future<void> batchInsertUsers(List<User> users) async {
final Database db = await DatabaseHelper.database;
Batch batch = db.batch();
for (User user in users) {
final Map<String, dynamic> userMap = user.toMap();
userMap.remove(DatabaseHelper.columnUserId);
batch.insert(
DatabaseHelper.tableUsers,
userMap,
);
}
try {
List<dynamic> results = await batch.commit(noResult: false);
print('Batch processing completed: Created ${results.length} users');
} catch (e) {
print('Batch processing error: $e');
rethrow;
}
}
// Complex transaction example
static Future<void> transferPostsBetweenUsers(int fromUserId, int toUserId) async {
final Database db = await DatabaseHelper.database;
await db.transaction((txn) async {
// Check user existence
final fromUser = await txn.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [fromUserId],
);
final toUser = await txn.query(
DatabaseHelper.tableUsers,
where: '${DatabaseHelper.columnUserId} = ?',
whereArgs: [toUserId],
);
if (fromUser.isEmpty || toUser.isEmpty) {
throw Exception('Specified user not found');
}
// Transfer posts
int updatedCount = await txn.update(
DatabaseHelper.tablePosts,
{DatabaseHelper.columnPostUserId: toUserId},
where: '${DatabaseHelper.columnPostUserId} = ?',
whereArgs: [fromUserId],
);
print('Transferred $updatedCount posts');
});
}
}
Practical Flutter Widget Integration
// User list screen
class UserListScreen extends StatefulWidget {
@override
_UserListScreenState createState() => _UserListScreenState();
}
class _UserListScreenState extends State<UserListScreen> {
List<User> users = [];
bool isLoading = true;
@override
void initState() {
super.initState();
_loadUsers();
}
Future<void> _loadUsers() async {
try {
final loadedUsers = await UserRepository.getAllUsers();
setState(() {
users = loadedUsers;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
_showErrorDialog('User loading error: $e');
}
}
Future<void> _addUser() async {
final user = User(
name: 'Test User ${DateTime.now().millisecondsSinceEpoch}',
email: 'test${DateTime.now().millisecondsSinceEpoch}@example.com',
age: 25,
createdAt: DateTime.now(),
);
try {
await UserRepository.insertUser(user);
_loadUsers(); // Reload list
} catch (e) {
_showErrorDialog('User creation error: $e');
}
}
Future<void> _deleteUser(int userId) async {
try {
bool deleted = await UserRepository.deleteUser(userId);
if (deleted) {
_loadUsers(); // Reload list
}
} catch (e) {
_showErrorDialog('User deletion error: $e');
}
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('User List'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadUsers,
),
],
),
body: isLoading
? Center(child: CircularProgressIndicator())
: users.isEmpty
? Center(child: Text('No users found'))
: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
leading: CircleAvatar(
child: Text(user.name.substring(0, 1)),
),
title: Text(user.name),
subtitle: Text('${user.email} • Age: ${user.age ?? 'Unknown'}'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteUser(user.id!),
),
onTap: () {
// Navigate to user detail screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserDetailScreen(user: user),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addUser,
child: Icon(Icons.add),
),
);
}
}
// User detail screen
class UserDetailScreen extends StatefulWidget {
final User user;
UserDetailScreen({required this.user});
@override
_UserDetailScreenState createState() => _UserDetailScreenState();
}
class _UserDetailScreenState extends State<UserDetailScreen> {
List<Post> posts = [];
bool isLoading = true;
@override
void initState() {
super.initState();
_loadUserPosts();
}
Future<void> _loadUserPosts() async {
try {
final userPosts = await PostRepository.getPostsByUserId(widget.user.id!);
setState(() {
posts = userPosts;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.user.name),
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16.0),
child: Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('User Information', style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 8),
Text('Name: ${widget.user.name}'),
Text('Email: ${widget.user.email}'),
Text('Age: ${widget.user.age ?? 'Unknown'}'),
Text('Created: ${widget.user.createdAt.toString().substring(0, 19)}'),
],
),
),
),
),
Expanded(
child: isLoading
? Center(child: CircularProgressIndicator())
: posts.isEmpty
? Center(child: Text('No posts found'))
: ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
title: Text(post.title),
subtitle: Text(
post.content.length > 100
? '${post.content.substring(0, 100)}...'
: post.content,
),
trailing: Text(
post.createdAt.toString().substring(0, 16),
style: Theme.of(context).textTheme.bodySmall,
),
),
);
},
),
),
],
),
);
}
}