Local Storage
Hive, SharedPreferences, and Drift patterns for local data persistence in Flutter
You are an expert in local data persistence for building cross-platform apps with Flutter. ## Key Points - **Choose the right tool for the job**: SharedPreferences for flat settings, Hive for structured object caching, Drift for relational data with complex queries. - **Never store secrets in SharedPreferences or Hive.** Use `flutter_secure_storage` for tokens, passwords, and API keys. - **Run Drift on a background isolate** using `NativeDatabase.createInBackground` to keep the UI thread responsive. - **Version your Hive type adapters.** Never reorder or remove `@HiveField` annotations — only append new fields. - **Write migration tests** for Drift schema changes to catch issues before they hit production. - **Calling SharedPreferences.getInstance() repeatedly**: It returns a Future that does disk I/O. Initialize once at app startup and inject the instance. - **Storing large blobs in SharedPreferences**: It is backed by XML on Android and plist on iOS. Large values degrade startup performance. - **Forgetting to open Hive boxes before access**: Calling `Hive.box()` on a closed box throws. Open all required boxes during app initialization. - **Not handling Drift schema downgrades**: If a user installs an older version, the database schema version may be higher than expected. Handle this gracefully. - **Blocking the main isolate with large Hive writes**: Batch large writes with `putAll` and consider moving to a background isolate for bulk operations.
skilldb get flutter-skills/Local StorageFull skill: 280 linesLocal Storage — Flutter
You are an expert in local data persistence for building cross-platform apps with Flutter.
Core Philosophy
Local storage in Flutter is not one tool -- it is a spectrum of tools matched to different data shapes and access patterns. SharedPreferences is for flat key-value settings (theme, locale, feature flags). Hive is for structured object caching where you need fast reads without SQL overhead. Drift is for relational data with complex queries, joins, and migrations. Choosing the wrong tool creates problems that are expensive to fix later: SharedPreferences for a 10,000-record dataset degrades startup performance; Drift for a single boolean preference adds unnecessary complexity.
Security is a separate concern from storage. SharedPreferences stores data in plaintext XML (Android) or plist (iOS). Hive can encrypt its boxes, but the encryption key must itself be stored securely. Neither is appropriate for authentication tokens, API keys, or sensitive user data. Use flutter_secure_storage, which leverages the platform's secure enclave (Keychain on iOS, EncryptedSharedPreferences on Android) for data that must not be accessible to a jailbroken device or a rooted phone.
Reactive data access is the pattern that keeps Flutter UIs in sync with local data. Drift's watch() methods return Stream<List<T>> that emit new values whenever the underlying table changes. Hive's listenable() enables ValueListenableBuilder for automatic UI updates. When the storage layer is reactive, there is no need for manual "refresh" calls or stale data problems -- the UI rebuilds automatically when the data changes, regardless of where the change originated.
Anti-Patterns
-
Calling SharedPreferences.getInstance() on every read:
SharedPreferences.getInstance()returns aFuturethat performs disk I/O. Calling it repeatedly in hot paths (widget builds, per-item processing) adds unnecessary latency. Initialize it once during app startup, store the instance, and inject it into repositories. -
Storing large datasets in SharedPreferences: SharedPreferences is backed by XML (Android) and plist (iOS), which are loaded entirely into memory on first access. A SharedPreferences file with thousands of entries slows app startup measurably. Use Hive or Drift for datasets larger than a few dozen entries.
-
Forgetting to open Hive boxes before accessing them: Calling
Hive.box<T>('name')on a box that has not been opened throws an exception. All required boxes must be opened during app initialization (typically inmain()) before any widget attempts to read from them. -
Storing secrets in SharedPreferences or unencrypted Hive: Authentication tokens, API keys, and personal data stored in plaintext SharedPreferences or unencrypted Hive boxes are accessible on rooted/jailbroken devices and in device backups. Use
flutter_secure_storagefor any data that must remain confidential. -
Blocking the main isolate with large Hive writes: Writing thousands of objects to a Hive box blocks the main isolate, causing UI jank. Use
putAll()for batch operations and consider moving large writes to a background isolate withIsolate.run().
Overview
Flutter apps frequently need to store data locally — user preferences, cached API responses, offline-first databases. This skill covers three key tools: SharedPreferences for simple key-value settings, Hive for fast NoSQL object storage, and Drift for full SQL databases with type-safe queries.
Core Concepts
SharedPreferences — Simple Key-Value Storage
Best for small amounts of flat data: user settings, feature flags, onboarding state.
class SettingsRepository {
SettingsRepository(this._prefs);
final SharedPreferences _prefs;
static const _themeKey = 'theme_mode';
static const _localeKey = 'locale';
ThemeMode get themeMode {
final value = _prefs.getString(_themeKey);
return ThemeMode.values.firstWhere(
(m) => m.name == value,
orElse: () => ThemeMode.system,
);
}
Future<void> setThemeMode(ThemeMode mode) => _prefs.setString(_themeKey, mode.name);
Locale? get locale {
final code = _prefs.getString(_localeKey);
return code != null ? Locale(code) : null;
}
Future<void> setLocale(Locale locale) => _prefs.setString(_localeKey, locale.languageCode);
}
Hive — Fast NoSQL Object Storage
Hive stores Dart objects in binary format. Ideal for caching structured data, user sessions, and offline queues.
// Register adapter (generated by build_runner with hive_generator)
@HiveType(typeId: 0)
class CachedArticle extends HiveObject {
@HiveField(0)
late String id;
@HiveField(1)
late String title;
@HiveField(2)
late String body;
@HiveField(3)
late DateTime cachedAt;
}
// Initialization
Future<void> initHive() async {
final dir = await getApplicationDocumentsDirectory();
Hive.init(dir.path);
Hive.registerAdapter(CachedArticleAdapter());
await Hive.openBox<CachedArticle>('articles');
}
// Repository
class ArticleCacheRepository {
Box<CachedArticle> get _box => Hive.box<CachedArticle>('articles');
List<CachedArticle> getAll() => _box.values.toList();
CachedArticle? getById(String id) => _box.get(id);
Future<void> save(CachedArticle article) => _box.put(article.id, article);
Future<void> saveAll(List<CachedArticle> articles) async {
final map = {for (final a in articles) a.id: a};
await _box.putAll(map);
}
Future<void> clearStale(Duration maxAge) async {
final cutoff = DateTime.now().subtract(maxAge);
final staleKeys = _box.keys.where((key) {
final article = _box.get(key);
return article != null && article.cachedAt.isBefore(cutoff);
}).toList();
await _box.deleteAll(staleKeys);
}
}
Drift — Type-Safe SQL Database
Drift generates Dart code from SQL table definitions, giving compile-time query validation and reactive streams.
// Define tables
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 200)();
TextColumn get description => text().nullable()();
BoolColumn get completed => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
// Define the database
@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
AppDatabase(super.e);
@override
int get schemaVersion => 1;
// Queries
Future<List<Todo>> getAllTodos() => select(todos).get();
Stream<List<Todo>> watchIncompleteTodos() {
return (select(todos)
..where((t) => t.completed.equals(false))
..orderBy([(t) => OrderingTerm.desc(t.createdAt)]))
.watch();
}
Future<int> insertTodo(TodosCompanion entry) => into(todos).insert(entry);
Future<bool> updateTodo(Todo entry) => update(todos).replace(entry);
Future<int> deleteTodo(int id) {
return (delete(todos)..where((t) => t.id.equals(id))).go();
}
}
Drift Database Initialization
AppDatabase constructDb() {
final db = LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'app.sqlite'));
return NativeDatabase.createInBackground(file);
});
return AppDatabase(db);
}
Implementation Patterns
Offline-First with Hive Cache
class ArticleRepository {
ArticleRepository(this._api, this._cache);
final ApiClient _api;
final ArticleCacheRepository _cache;
Future<List<Article>> getArticles({bool forceRefresh = false}) async {
if (!forceRefresh) {
final cached = _cache.getAll();
if (cached.isNotEmpty) {
// Return cache immediately, refresh in background
_refreshFromNetwork();
return cached.map(_toDomain).toList();
}
}
try {
final articles = await _fetchFromNetwork();
return articles;
} catch (e) {
// Fallback to cache on network failure
final cached = _cache.getAll();
if (cached.isNotEmpty) return cached.map(_toDomain).toList();
rethrow;
}
}
Future<List<Article>> _fetchFromNetwork() async {
final response = await _api.get<List>('/articles');
final articles = response.data!.map((j) => Article.fromJson(j)).toList();
await _cache.saveAll(articles.map(_toCached).toList());
return articles;
}
}
Drift Migrations
@override
int get schemaVersion => 2;
@override
MigrationStrategy get migration {
return MigrationStrategy(
onCreate: (m) async {
await m.createAll();
},
onUpgrade: (m, from, to) async {
if (from < 2) {
await m.addColumn(todos, todos.description);
}
},
);
}
Reactive UI with Drift Streams
class TodoListScreen extends StatelessWidget {
const TodoListScreen({super.key});
@override
Widget build(BuildContext context) {
final db = context.read<AppDatabase>();
return StreamBuilder<List<Todo>>(
stream: db.watchIncompleteTodos(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
final todos = snapshot.data!;
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => TodoTile(todo: todos[index]),
);
},
);
}
}
Best Practices
- Choose the right tool for the job: SharedPreferences for flat settings, Hive for structured object caching, Drift for relational data with complex queries.
- Never store secrets in SharedPreferences or Hive. Use
flutter_secure_storagefor tokens, passwords, and API keys. - Run Drift on a background isolate using
NativeDatabase.createInBackgroundto keep the UI thread responsive. - Version your Hive type adapters. Never reorder or remove
@HiveFieldannotations — only append new fields. - Write migration tests for Drift schema changes to catch issues before they hit production.
Common Pitfalls
- Calling SharedPreferences.getInstance() repeatedly: It returns a Future that does disk I/O. Initialize once at app startup and inject the instance.
- Storing large blobs in SharedPreferences: It is backed by XML on Android and plist on iOS. Large values degrade startup performance.
- Forgetting to open Hive boxes before access: Calling
Hive.box()on a closed box throws. Open all required boxes during app initialization. - Not handling Drift schema downgrades: If a user installs an older version, the database schema version may be higher than expected. Handle this gracefully.
- Blocking the main isolate with large Hive writes: Batch large writes with
putAlland consider moving to a background isolate for bulk operations.
Install this skill directly: skilldb add flutter-skills
Related Skills
Animations
Implicit, explicit, and hero animation patterns for polished Flutter UIs
Navigation
GoRouter navigation patterns for declarative, deep-linkable Flutter routing
Networking
Dio HTTP client and API integration patterns for Flutter applications
Platform Channels
Platform channels for bridging Flutter with native Android (Kotlin) and iOS (Swift) code
State Management
Riverpod and Bloc state management patterns for scalable Flutter applications
Testing
Widget testing, unit testing, and integration testing patterns for Flutter apps