Skip to content

Local Storage & Database Management


Effective local data storage is crucial for creating responsive, offline-capable Flutter applications. This guide covers choosing and implementing the right storage solution for your needs.


Quick decision guide:

  • Settings & preferences → SharedPreferences
  • Simple object storage → Hive
  • Complex relationships → Drift (SQLite)
  • Raw SQL control → SQFlite

SharedPreferences - Simple key-value storage for primitive data. Perfect for user settings and small data pieces.

Hive - Fast NoSQL database built for Flutter. Great for moderate-sized structured data without relationships.

Drift (formerly Moor) - Type-safe SQLite wrapper with code generation. Ideal for relational data with compile-time safety.

SQFlite - Direct SQLite bindings for Flutter. Provides low-level control for complex SQL operations.


Decision framework:

  1. Basic Configuration or Simple Data Use SharedPreferences for small, non-structured data like settings and flags.

  2. Moderate Structured Data Choose Hive for storing objects without complex relationships when performance is critical.

  3. Relational Data with Type Safety Select Drift for complex, relational data that benefits from compile-time checking.

  4. Complex SQL Requirements Use SQFlite when you need direct SQL control or are migrating from an existing SQLite database.


SharedPreferences - Simple Key-Value Storage

Section titled “SharedPreferences - Simple Key-Value Storage”

Best for: User settings, app preferences, simple flags

// pubspec.yaml
dependencies:
shared_preferences: ^2.2.0
Storing Data
// Storing different data types
Future<void> saveUserPreferences() async {
final prefs = await SharedPreferences.getInstance();
// Store basic types
await prefs.setString('username', 'flutter_user');
await prefs.setBool('darkMode', true);
await prefs.setInt('launchCount', 10);
await prefs.setStringList('recentSearches', ['flutter', 'dart']);
}
Reading Data
// Retrieving values with defaults
Future<void> loadUserPreferences() async {
final prefs = await SharedPreferences.getInstance();
final username = prefs.getString('username') ?? 'Guest';
final isDarkMode = prefs.getBool('darkMode') ?? false;
final launchCount = prefs.getInt('launchCount') ?? 0;
// Increment launch count
await prefs.setInt('launchCount', launchCount + 1);
}

Use constants for keys to avoid typos:

class PreferenceKeys {
static const String username = 'username';
static const String darkMode = 'darkMode';
static const String launchCount = 'launchCount';
}
// Usage
await prefs.setBool(PreferenceKeys.darkMode, true);

Create a service class for better organization:

Preferences Service
class PreferencesService {
late SharedPreferences _prefs;
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
// Getters
String get username => _prefs.getString(PreferenceKeys.username) ?? 'Guest';
bool get isDarkMode => _prefs.getBool(PreferenceKeys.darkMode) ?? false;
// Setters
Future<void> setUsername(String value) =>
_prefs.setString(PreferenceKeys.username, value);
Future<void> setDarkMode(bool value) =>
_prefs.setBool(PreferenceKeys.darkMode, value);
}

Limitations of SharedPreferences:

  • Only supports primitive types (bool, int, double, String) and String lists
  • Not suitable for structured data or objects
  • Not designed for storing large amounts of data
  • Limited thread safety for high-frequency operations

Hive is a lightweight, high-performance NoSQL database optimized for Flutter applications.

Dependencies
// pubspec.yaml
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
Model Definition
import 'package:hive/hive.dart';
@HiveType(typeId: 0)
class Task {
@HiveField(0)
final String id;
@HiveField(1)
String title;
@HiveField(2)
bool completed;
Task({required this.id, required this.title, this.completed = false});
}
Basic Usage
// Initialize and use Hive
await Hive.initFlutter();
Hive.registerAdapter(TaskAdapter());
final taskBox = await Hive.openBox<Task>('tasks');
// CRUD operations
await taskBox.put(task.id, task); // Create/Update
final task = taskBox.get(id); // Read
await taskBox.delete(id); // Delete

Hive Best Practices:

  • Use unique typeIds for each model class (0-223)
  • Implement repository pattern to encapsulate database logic
  • Close boxes when they’re no longer needed
  • Use Hive’s lazy boxes for large collections
  • Consider encryption for sensitive data with hive_flutter.encryptionCipher

When Not to Use Hive:

  • When you need complex relational queries
  • When data consistency is critical during crashes
  • For very large datasets where indexing is important

Drift is a reactive persistence library for Dart & Flutter applications that builds on top of SQLite.

Dependencies
// pubspec.yaml
dependencies:
drift: ^2.8.0
sqlite3_flutter_libs: ^0.5.15
Database Setup
// Define table and database
class Tasks extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text()();
BoolColumn get completed => boolean().withDefault(const Constant(false))();
}
@DriftDatabase(tables: [Tasks])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
// CRUD operations
Future<List<Task>> getAllTasks() => select(tasks).get();
Stream<List<Task>> watchAllTasks() => select(tasks).watch();
Future<int> createTask(TasksCompanion task) => into(tasks).insert(task);
}
Reactive UI
// Reactive UI updates
StreamBuilder<List<Task>>(
stream: database.watchAllTasks(),
builder: (context, snapshot) {
final tasks = snapshot.data ?? [];
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) => ListTile(
title: Text(tasks[index].title),
leading: Checkbox(
value: tasks[index].completed,
onChanged: (value) => database.updateTask(
tasks[index].copyWith(completed: Value(value ?? false))
),
),
),
);
},
)

Drift Best Practices:

  • Define database schema in a dedicated file
  • Use migrations for schema updates
  • Leverage reactive streams for UI updates
  • Implement repository pattern to abstract database operations
  • Use transactions for related operations
Drift Transaction Example
// Example of using transactions in Drift
Future<void> transferFunds(int fromAccount, int toAccount, double amount) {
return database.transaction(() async {
// Deduct from source account
await (update(accounts)..where((a) => a.id.equals(fromAccount)))
.write(AccountsCompanion(
balance: Expression.custom('balance - $amount')
));
// Add to destination account
await (update(accounts)..where((a) => a.id.equals(toAccount)))
.write(AccountsCompanion(
balance: Expression.custom('balance + $amount')
));
// Create transaction record
await into(transactions).insert(
TransactionsCompanion.insert(
fromAccountId: fromAccount,
toAccountId: toAccount,
amount: amount,
date: DateTime.now(),
)
);
});
}

SQFlite provides direct access to SQLite functionality in Flutter applications.

Dependencies
// pubspec.yaml
dependencies:
sqflite: ^2.2.8+4
path: ^1.8.3
Database Setup
// Database helper singleton
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('app_database.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(path, version: 1, onCreate: _createDB);
}
Future<void> _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE tasks(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER NOT NULL
)
''');
}
}
Basic Operations
// CRUD operations
Future<int> createTask(String title) async {
final db = await database;
return await db.insert('tasks', {'title': title, 'completed': 0});
}
Future<List<Map<String, dynamic>>> readAllTasks() async {
final db = await database;
return await db.query('tasks');
}
Future<int> updateTask(int id, bool completed) async {
final db = await database;
return await db.update(
'tasks',
{'completed': completed ? 1 : 0},
where: 'id = ?',
whereArgs: [id],
);
}

SQFlite Best Practices:

  • Use a singleton pattern for database helper
  • Define SQL queries as constants
  • Handle migrations carefully for schema changes
  • Close database connections when no longer needed
  • Use transactions for complex operations

Drift vs SQFlite:

  • Drift provides compile-time type safety, eliminating SQL string errors
  • Drift generates boilerplate code for you
  • SQFlite gives more direct control over SQL queries
  • SQFlite has a lower learning curve for developers familiar with SQL

Implementing effective offline capabilities ensures your app remains functional without an internet connection.

  1. Local Persistence - Store all essential data locally for offline access
  2. Sync Management - Track changes made offline to sync later
  3. Conflict Resolution - Define strategies for handling conflicts during synchronization
  4. UI Feedback - Inform users about offline mode and sync status
class BasicSyncService {
final ApiService _api;
final TaskRepository _repository;
BasicSyncService(this._api, this._repository);
Future<void> syncTasks() async {
try {
// Download remote changes
final remoteTasks = await _api.fetchTasks();
for (var task in remoteTasks) {
await _repository.saveTask(task);
}
// Upload local changes
final localTasks = await _repository.getPendingUploads();
for (var task in localTasks) {
await _api.uploadTask(task);
await _repository.markAsSynced(task.id);
}
} catch (e) {
// Handle connectivity issues
print('Sync failed: $e');
}
}
}

When to Use:

  • For simple data models
  • When conflicts are rare
  • For apps with limited offline editing

Always prefer server data over local data during conflicts. Simple to implement but may result in lost offline changes.

Local changes override server data during conflicts. Preserves user changes but may overwrite others’ updates in collaborative scenarios.

Use timestamps to determine which change is more recent. Requires synchronized clocks but provides a logical resolution approach.

Present conflicts to users and let them decide how to resolve them. Most user-friendly but increases complexity significantly.


As your app evolves, you may need to migrate from one storage solution to another:

  1. Dual Write Period - Write to both old and new storage systems during transition
  2. Data Migration - Transfer existing data from old to new storage
  3. Validation - Verify data integrity in the new system
  4. Switch Read Path - Start reading from the new storage system
  5. Cleanup - Remove old storage system once migration is stable
Migration Example
// Migration service example
class StorageMigrationService {
final SharedPreferences _prefs;
final TaskRepository _hiveRepo;
Future<void> migrateTasksToHive() async {
final migrated = _prefs.getBool('migration_completed') ?? false;
if (migrated) return;
try {
// Get data from old storage
final taskJson = _prefs.getString('tasks') ?? '[]';
final tasks = jsonDecode(taskJson);
// Migrate to new storage
for (var taskMap in tasks) {
await _hiveRepo.addTask(Task.fromMap(taskMap));
}
// Mark as completed
await _prefs.setBool('migration_completed', true);
} catch (e) {
print('Migration failed: $e');
}
}
}

Overall Storage Guidelines:

  • Choose the right tool for each data type
  • Separate storage logic into repository classes
  • Implement proper error handling for storage operations
  • Use transactions for related operations
  • Always handle migration paths for schema changes
  • Implement data backup strategies for critical user data

Common Mistakes to Avoid:

  • Storing large data structures in SharedPreferences
  • Performing database operations on the UI thread
  • Neglecting to close database connections
  • Overcomplicating the storage layer for simple needs
  • Missing error handling for storage failures

  1. SharedPreferences Documentation – Flutter’s key-value storage solution
  2. Hive Documentation – Fast, lightweight NoSQL database for Flutter
  3. Drift Documentation – Type-safe SQLite wrapper for Flutter
  4. SQFlite Documentation – SQLite plugin for Flutter
  5. State Management – Related guide on state management