← Articles

Local storage options in Flutter: a comparison

By Ann Tech · 22 September 2025

Flutter gives you several options for local storage, each with different performance, query capabilities, and complexity trade-offs. Here is a practical comparison to help you choose the right one.

The options

PackageTypeQueryPerformanceBest for
shared_preferencesKey-valueNoFastApp settings, small values
hiveKey-value + boxesBasicVery fastNon-relational cached data
driftSQL (SQLite)Full SQLMediumRelational data, complex queries
isarNoSQLDart APIVery fastLarge datasets, complex indexes
flutter_secure_storageKey-value (encrypted)NoMediumTokens, passwords

shared_preferences: settings and flags

The simplest option. Data is stored as platform-specific key-value pairs (NSUserDefaults on iOS, SharedPreferences on Android).

final prefs = await SharedPreferences.getInstance();

// Write
await prefs.setString('user_name', 'Alice');
await prefs.setBool('notifications_enabled', true);
await prefs.setInt('theme_mode', 0);

// Read
final name = prefs.getString('user_name') ?? 'Guest';
final notificationsEnabled = prefs.getBool('notifications_enabled') ?? true;

// Delete
await prefs.remove('user_name');
await prefs.clear(); // Remove all

Limitation: no reactive updates. You have to call getInstance() each time to get fresh values. For reactive settings, wrap in a ChangeNotifier or Riverpod provider:

final settingsProvider = StateNotifierProvider<SettingsNotifier, AppSettings>((ref) {
  return SettingsNotifier();
});

class SettingsNotifier extends StateNotifier<AppSettings> {
  SettingsNotifier() : super(const AppSettings()) {
    _load();
  }

  Future<void> _load() async {
    final prefs = await SharedPreferences.getInstance();
    state = AppSettings(
      isDarkMode: prefs.getBool('dark_mode') ?? false,
      notificationsEnabled: prefs.getBool('notifications') ?? true,
    );
  }

  Future<void> setDarkMode(bool value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('dark_mode', value);
    state = state.copyWith(isDarkMode: value);
  }
}

Hive: fast unstructured cache

Hive stores data in typed boxes using binary serialization. No SQL, no schema migrations, very fast reads.

// Setup
await Hive.initFlutter();
Hive.registerAdapter(OrderAdapter());

// Open a box
final box = await Hive.openBox<Order>('orders');

// Write
await box.put('order-1', Order(id: 'order-1', status: 'pending'));
await box.putAll({'order-2': order2, 'order-3': order3});

// Read
final order = box.get('order-1');
final allOrders = box.values.toList();

// Reactive
ValueListenableBuilder<Box<Order>>(
  valueListenable: Hive.box<Order>('orders').listenable(),
  builder: (context, box, _) {
    return ListView.builder(
      itemCount: box.length,
      itemBuilder: (_, i) => OrderCard(order: box.getAt(i)!),
    );
  },
)

Hive's TypeAdapter is generated by build_runner:

@HiveType(typeId: 0)
class Order extends HiveObject {
  @HiveField(0) late String id;
  @HiveField(1) late String status;
  @HiveField(2) late double totalAmount;
}

Weakness: no SQL queries. Finding all orders with status == 'pending' means loading all orders and filtering in Dart.

Drift: relational data with SQL

Drift (formerly Moor) gives you full SQLite with type-safe Dart queries. The right choice when you need joins, aggregations, or complex filtering.

// Reactive query with automatic re-emission when data changes
Stream<List<Order>> watchPendingOrders() {
  return (select(orders)..where((o) => o.status.equals('pending')))
      .watch();
}

// Aggregation
Future<double> getTotalRevenue() async {
  final sum = orders.totalAmount.sum();
  final query = selectOnly(orders)..addColumns([sum]);
  final result = await query.getSingle();
  return result.read(sum) ?? 0.0;
}

Isar: high-performance NoSQL

Isar is the fastest option for large datasets. No SQL, but has a powerful Dart-based query API:

final isar = await Isar.open([OrderSchema]);

// Write
await isar.writeTxn(() async {
  await isar.orders.put(order);
});

// Query with index
final pending = await isar.orders
    .filter()
    .statusEqualTo('pending')
    .sortByCreatedAtDesc()
    .findAll();

// Reactive
isar.orders
    .filter()
    .statusEqualTo('pending')
    .watch(fireImmediately: true)
    .listen((orders) {
      // Update UI
    });

Decision guide

Need to store sensitive data (tokens, passwords)?
  → flutter_secure_storage

Storing app settings/preferences?
  → shared_preferences

Caching API responses, non-relational data, need speed?
  → hive or isar

Relational data with complex queries?
  → drift

Large dataset (10k+ records) with complex filters?
  → isar

Combining multiple options

Production apps typically use a combination:

@module
abstract class StorageModule {
  // Settings
  @preResolve @singleton
  Future<SharedPreferences> get prefs => SharedPreferences.getInstance();

  // Secure tokens
  @singleton
  FlutterSecureStorage get secureStorage => const FlutterSecureStorage();

  // Offline cache
  @singleton
  Box<Order> get orderCache => Hive.box<Order>('orders');

  // Structured data
  @singleton
  AppDatabase get database => AppDatabase();
}

Common pitfalls

Using shared_preferences for large data. It loads everything into memory on init. If you store 1000 orders in preferences, the app startup slows noticeably.

Hive without proper TypeAdapters. Hive can store primitive types without adapters, but complex objects need registered adapters. Forgetting Hive.registerAdapter() before openBox() throws at runtime.

Not closing Hive boxes. Open boxes hold file handles. Call box.close() in dispose or let Hive.close() clean up everything on app exit.

SQLite on the main thread. Drift's NativeDatabase.createInBackground() moves database operations off the UI thread. The default NativeDatabase() runs on the main isolate and can cause jank.

Sign in to like, dislike, or report.

Local storage options in Flutter: a comparison — ANN Tech