Skip to main content
Technology & EngineeringFlutter280 lines

Local Storage

Hive, SharedPreferences, and Drift patterns for local data persistence in Flutter

Quick Summary16 lines
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 lines
Paste into your CLAUDE.md or agent config

Local 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 a Future that 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 in main()) 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_storage for 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 with Isolate.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_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.

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 putAll and consider moving to a background isolate for bulk operations.

Install this skill directly: skilldb add flutter-skills

Get CLI access →