SQLite
A self-contained, high-reliability, embedded SQL database engine — perfect for complex data and relationships.
SQLite is a self-contained, high-reliability, embedded, SQL database engine. It's the most widely deployed database in the world and is perfect for Flutter apps that need:
- Complex data structures with multiple tables and relationships
- Efficient queries with filtering, sorting, and joining
- Large datasets that need to be queried quickly
- Data integrity with constraints and transactions
✅ When to Use SQLite
- Complex data with multiple related tables
- Need for complex queries (JOIN, GROUP BY, ORDER BY)
- Large datasets that need efficient searching
- Data integrity with foreign keys and constraints
- When you need transactions for data consistency
Hive
- Key-value NoSQL database
- Simple API, like a Map
- No SQL required
- Great for objects and simple data
- Real-time updates with listenable
- Faster for simple operations
SQLite
- Relational SQL database
- Complex queries with SQL
- Multiple tables with relationships
- Great for complex data and reports
- Transactions and data integrity
- Better for large datasets
SharedPreferences
- Simple key-value storage
- Primitive types only
- Small data only
- Best for app settings
💡 Which One to Choose?
- Use SQLite – for complex data, relationships, and complex queries
- Use Hive – for simple object storage with real-time updates
- Use SharedPreferences – for simple app settings and preferences
To use SQLite in Flutter, you need to add the
sqflite
and
path
packages.
dependencies:
sqflite: ^2.4.3
path: ^1.9.0
import
'dart:async'
;
import
'package:sqflite/sqflite.dart'
;
import
'package:path/path.dart'
;
class
DatabaseHelper
{
static
final
DatabaseHelper _instance = DatabaseHelper._internal();
factory
DatabaseHelper() => _instance;
DatabaseHelper._internal();
static
Database? _database;
Future
<Database>
get
database async {
if
(_database !=
null
)
return
_database!;
_database =
await
_initDatabase();
return
_database!;
}
Future
<Database>
_initDatabase
() async {
final
databasesPath = await getDatabasesPath();
final
path = join(databasesPath,
'app_database.db'
);
return
await openDatabase(
path,
version:
1
,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future
<
void
>
_onCreate
(Database db,
int
version) async {
// Create tables here
await
db.execute(
'''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER
)
'''
);
await
db.execute(
'''
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
'''
);
// Insert sample data
await
_insertSampleData(db);
}
Future
<
void
>
_onUpgrade
(Database db,
int
oldVersion,
int
newVersion) async {
// Handle database migrations
if
(oldVersion <
2
) {
await
db.execute(
'ALTER TABLE users ADD COLUMN phone TEXT'
);
}
}
Future
<
void
>
_insertSampleData
(Database db) async {
// Insert sample users
await
db.insert(
'users'
, {
'name'
:
'Alice Johnson'
,
'email'
:
'alice@example.com'
,
'age'
:
28
,
});
await
db.insert(
'users'
, {
'name'
:
'Bob Smith'
,
'email'
:
'bob@example.com'
,
'age'
:
34
,
});
}
}
Here are the basic CRUD (Create, Read, Update, Delete) operations for SQLite in Flutter.
class
User
{
final
int
? id;
final
String name;
final
String email;
final
int
? age;
User
({
this
.id,
required
this
.name,
required
this
.email,
this
.age});
// Convert to Map for database
Map<String, dynamic>
toMap
() {
return
{
'id'
: id,
'name'
: name,
'email'
: email,
'age'
: age,
};
}
// Create from Map
factory User.fromMap(Map<String, dynamic> map) {
return
User(
id: map[
'id'
],
name: map[
'name'
],
email: map[
'email'
],
age: map[
'age'
],
);
}
@override
String
toString
() {
return
'User{id: $id, name: $name, email: $email, age: $age}'
;
}
}
import
'package:sqflite/sqflite.dart'
;
import
'../models/user.dart'
;
import
'database_helper.dart'
;
class
UserDao
{
final
DatabaseHelper _dbHelper = DatabaseHelper();
// ----- CREATE -----
Future
<
int
>
insertUser
(User user) async {
final
db = await _dbHelper.database;
return
await db.insert(
'users'
, user.toMap());
}
// ----- READ (All) -----
Future
<List<User>>
getAllUsers
() async {
final
db = await _dbHelper.database;
final
List<Map<String, dynamic>> maps = await db.query(
'users'
);
return
maps.map((map) => User.fromMap(map)).toList();
}
// ----- READ (Single) -----
Future
<User?>
getUserById
(
int
id) async {
final
db = await _dbHelper.database;
final
List<Map<String, dynamic>> maps =
await
db.query(
'users'
,
where:
'id = ?'
,
whereArgs: [id],
);
if
(maps.isNotEmpty) {
return
User.fromMap(maps.first);
}
return
null
;
}
// ----- READ (Filtered) -----
Future
<List<User>>
getUsersByName
(String name) async {
final
db = await _dbHelper.database;
final
List<Map<String, dynamic>> maps =
await
db.query(
'users'
,
where:
'name LIKE ?'
,
whereArgs: [
'%$name%'
],
);
return
maps.map((map) => User.fromMap(map)).toList();
}
// ----- UPDATE -----
Future
<
int
>
updateUser
(User user) async {
final
db = await _dbHelper.database;
return
await db.update(
'users'
,
user.toMap(),
where:
'id = ?'
,
whereArgs: [user.id],
);
}
// ----- DELETE -----
Future
<
int
>
deleteUser
(
int
id) async {
final
db = await _dbHelper.database;
return
await db.delete(
'users'
,
where:
'id = ?'
,
whereArgs: [id],
);
}
// ----- BATCH INSERT -----
Future
<
void
>
insertMultipleUsers
(List<User> users) async {
final
db = await _dbHelper.database;
Batch batch = db.batch();
for
(
var
user
in
users) {
batch.insert(
'users'
, user.toMap());
}
await
batch.commit();
}
}
SQLite supports complex queries with JOINs, GROUP BY, ORDER BY, and more.
import
'package:sqflite/sqflite.dart'
;
import
'../models/user.dart'
;
import
'database_helper.dart'
;
class
QueryDao
{
final
DatabaseHelper _dbHelper = DatabaseHelper();
// ----- JOIN Query (Users with their posts) -----
Future
<List<Map<String, dynamic>>>
getUsersWithPosts
() async {
final
db = await _dbHelper.database;
return
await
db.rawQuery(
'''
SELECT
users.id as user_id,
users.name,
users.email,
posts.id as post_id,
posts.title,
posts.content,
posts.created_at
FROM users
LEFT JOIN posts ON users.id = posts.user_id
ORDER BY users.name, posts.created_at DESC
'''
);
}
// ----- Aggregate Query (Count posts per user) -----
Future
<List<Map<String, dynamic>>>
getPostCountPerUser
() async {
final
db = await _dbHelper.database;
return
await
db.rawQuery(
'''
SELECT
users.id,
users.name,
COUNT(posts.id) as post_count
FROM users
LEFT JOIN posts ON users.id = posts.user_id
GROUP BY users.id
ORDER BY post_count DESC
'''
);
}
// ----- Pagination -----
Future
<List<User>>
getUsersPaginated
(
int
page,
int
limit) async {
final
db = await _dbHelper.database;
final
offset = (page -
1
) * limit;
final
List<Map<String, dynamic>> maps =
await
db.query(
'users'
,
limit: limit,
offset: offset,
orderBy:
'name ASC'
,
);
return
maps.map((map) => User.fromMap(map)).toList();
}
// ----- Search with multiple conditions -----
Future
<List<User>>
searchUsers
({
String? name,
String? email,
int
? minAge,
int
? maxAge,
}) async {
final
db = await _dbHelper.database;
List<String> conditions = [];
List<dynamic> args = [];
if
(name !=
null
&& name.isNotEmpty) {
conditions.add(
'name LIKE ?'
);
args.add(
'%$name%'
);
}
if
(email !=
null
&& email.isNotEmpty) {
conditions.add(
'email LIKE ?'
);
args.add(
'%$email%'
);
}
if
(minAge !=
null
) {
conditions.add(
'age >= ?'
);
args.add(minAge);
}
if
(maxAge !=
null
) {
conditions.add(
'age <= ?'
);
args.add(maxAge);
}
final
whereClause = conditions.isNotEmpty ? conditions.join(
' AND '
) :
null
;
final
List<Map<String, dynamic>> maps =
await
db.query(
'users'
,
where: whereClause,
whereArgs: args.isNotEmpty ? args :
null
,
);
return
maps.map((map) => User.fromMap(map)).toList();
}
}
Transactions ensure data consistency when performing multiple operations. If any operation fails, all changes are rolled back.
import
'package:sqflite/sqflite.dart'
;
import
'../models/user.dart'
;
import
'database_helper.dart'
;
class
TransactionDao
{
final
DatabaseHelper _dbHelper = DatabaseHelper();
// ----- Transaction: Delete user and all their posts -----
Future
<
void
>
deleteUserWithPosts
(
int
userId) async {
final
db = await _dbHelper.database;
await
db.transaction((txn) async {
// Delete posts first (due to foreign key)
await
txn.delete(
'posts'
,
where:
'user_id = ?'
,
whereArgs: [userId],
);
// Then delete the user
await
txn.delete(
'users'
,
where:
'id = ?'
,
whereArgs: [userId],
);
});
}
// ----- Transaction: Transfer user data to another user -----
Future
<
void
>
transferUserData
(
int
fromUserId,
int
toUserId) async {
final
db = await _dbHelper.database;
await
db.transaction((txn) async {
// Update posts to new user
await
txn.update(
'posts'
,
{
'user_id'
: toUserId},
where:
'user_id = ?'
,
whereArgs: [fromUserId],
);
// Delete the old user
await
txn.delete(
'users'
,
where:
'id = ?'
,
whereArgs: [fromUserId],
);
});
}
// ----- Transaction with rollback on error -----
Future
<
bool
>
updateUserWithRollback
(User user) async {
final
db = await _dbHelper.database;
try
{
await
db.transaction((txn) async {
// Update user
await
txn.update(
'users'
,
user.toMap(),
where:
'id = ?'
,
whereArgs: [user.id],
);
// If validation fails, throw an error to rollback
if
(user.name.isEmpty) {
throw
Exception(
'Name cannot be empty'
);
}
});
return
true
;
}
catch
(e) {
return
false
;
}
}
}
Here's a complete Flutter app that uses SQLite for user management with CRUD operations.
import
'package:flutter/material.dart'
;
import
'package:sqflite/sqflite.dart'
;
import
'package:path/path.dart'
;
// ==========================================
// 1. MODEL
// ==========================================
class
User
{
final
int
? id;
final
String name;
final
String email;
final
int
? age;
User
({
this
.id,
required
this
.name,
required
this
.email,
this
.age});
Map<String, dynamic>
toMap
() {
return
{
'id'
: id,
'name'
: name,
'email'
: email,
'age'
: age,
};
}
factory User.fromMap(Map<String, dynamic> map) {
return
User(
id: map[
'id'
],
name: map[
'name'
],
email: map[
'email'
],
age: map[
'age'
],
);
}
}
// ==========================================
// 2. DATABASE HELPER
// ==========================================
class
DatabaseHelper
{
static
final
DatabaseHelper _instance = DatabaseHelper._internal();
factory
DatabaseHelper() => _instance;
DatabaseHelper._internal();
static
Database? _database;
Future
<Database>
get
database async {
if
(_database !=
null
)
return
_database!;
_database =
await
_initDatabase();
return
_database!;
}
Future
<Database>
_initDatabase
() async {
final
databasesPath = await getDatabasesPath();
final
path = join(databasesPath,
'user_database.db'
);
return
await openDatabase(
path,
version:
1
,
onCreate: (db, version) async {
await
db.execute(
'''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER
)
'''
);
},
);
}
}
// ==========================================
// 3. USER DAO
// ==========================================
class
UserDao
{
final
DatabaseHelper _dbHelper = DatabaseHelper();
Future
<
int
>
insertUser
(User user) async {
final
db = await _dbHelper.database;
return
await db.insert(
'users'
, user.toMap());
}
Future
<List<User>>
getAllUsers
() async {
final
db = await _dbHelper.database;
final
List<Map<String, dynamic>> maps =
await
db.query(
'users'
);
return
maps.map((map) => User.fromMap(map)).toList();
}
Future
<
int
>
updateUser
(User user) async {
final
db = await _dbHelper.database;
return
await db.update(
'users'
,
user.toMap(),
where:
'id = ?'
,
whereArgs: [user.id],
);
}
Future
<
int
>
deleteUser
(
int
id) async {
final
db = await _dbHelper.database;
return
await db.delete(
'users'
,
where:
'id = ?'
,
whereArgs: [id],
);
}
}
// ==========================================
// 4. FLUTTER UI
// ==========================================
void
main
() => runApp(
MyApp
());
class
MyApp
extends
StatelessWidget {
@override
Widget
build
(BuildContext context) {
return
MaterialApp(
title:
'User Management'
,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3:
true
,
),
home: UserListScreen(),
);
}
}
class
UserListScreen
extends
StatefulWidget {
@override
State<UserListScreen>
createState
() => _UserListScreenState();
}
class
_UserListScreenState
extends
State<UserListScreen> {
final
UserDao _userDao = UserDao();
List<User> _users = [];
bool
_isLoading =
true
;
final
TextEditingController _nameController = TextEditingController();
final
TextEditingController _emailController = TextEditingController();
final
TextEditingController _ageController = TextEditingController();
@override
void
initState
() {
super
.initState();
_loadUsers();
}
Future
<
void
>
_loadUsers
() async {
setState(() => _isLoading =
true
);
final
users = await _userDao.getAllUsers();
setState(() {
_users = users;
_isLoading =
false
;
});
}
Future
<
void
>
_addUser
() async {
final
name = _nameController.text.trim();
final
email = _emailController.text.trim();
final
age = int.tryParse(_ageController.text.trim());
if
(name.isEmpty || email.isEmpty)
return
;
final
user = User(name: name, email: email, age: age);
await
_userDao.insertUser(user);
_nameController.clear();
_emailController.clear();
_ageController.clear();
await
_loadUsers();
}
Future
<
void
>
_deleteUser
(
int
id) async {
await
_userDao.deleteUser(id);
await
_loadUsers();
}
void
_showAddDialog
() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Add User'
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _nameController,
decoration: InputDecoration(
hintText:
'Name'
,
border: OutlineInputBorder(),
),
),
SizedBox(height:
8
),
TextField(
controller: _emailController,
decoration: InputDecoration(
hintText:
'Email'
,
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
SizedBox(height:
8
),
TextField(
controller: _ageController,
decoration: InputDecoration(
hintText:
'Age (optional)'
,
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Cancel'
),
),
ElevatedButton(
onPressed: () {
_addUser();
Navigator.pop(context);
},
child: Text(
'Add'
),
),
],
),
);
}
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(
title: Text(
'👥 Users'
),
centerTitle:
true
,
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadUsers,
),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: _users.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.people_outline, size:
80
, color: Colors.grey.shade400),
SizedBox(height:
16
),
Text(
'No users yet'
,
style: TextStyle(fontSize:
20
, fontWeight: FontWeight.w600),
),
Text(
'Tap the + button to add one'
,
style: TextStyle(color: Colors.grey.shade400),
),
],
),
)
: ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
final
user = _users[index];
return
Card(
margin: EdgeInsets.symmetric(horizontal:
16
, vertical:
4
),
child: ListTile(
leading: CircleAvatar(
child: Text(user.name[
0
].toUpperCase()),
),
title: Text(user.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user.email),
if
(user.age !=
null
)
Text(
'Age: ${user.age}'
,
style: TextStyle(color: Colors.grey.shade600),
),
],
),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => _deleteUser(user.id!),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _showAddDialog,
child: Icon(Icons.add),
),
);
}
}
sqflite
and
path
to your
pubspec.yaml
.
toMap()
and
fromMap()
methods for serialization.
FutureBuilder
or
setState
to display data from the database.
✅ Do's
- Use a singleton database helper to avoid opening multiple connections
- Use transactions for multiple operations
- Use parameterized queries to prevent SQL injection
- Use migrations for database schema changes
- Close the database when the app is terminated
- Use indexes for frequently queried columns
❌ Don'ts
- Don't use raw SQL with string concatenation (use parameterized queries)
- Don't forget to handle exceptions in database operations
- Don't use SQLite for very simple data (use SharedPreferences)
- Don't open the database multiple times
Not closing the database can cause memory leaks and connection issues.
await db.close();
when the app is terminated or the database is no longer needed.
If you insert multiple related records without a transaction, partial failures can corrupt your data.
Wrap multiple operations in
db.transaction()
to ensure atomicity.
When you change the schema, users with existing databases need a migration path.
Use the
onUpgrade
callback in
openDatabase()
to handle schema migrations.
🎯 Key Takeaway
SQLite is a powerful relational database for Flutter apps that need complex data structures , relationships , and efficient queries . Use transactions for data consistency, parameterized queries for security, and migrations for schema updates. SQLite is the best choice for complex, relational data.