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
| Package | Type | Query | Performance | Best for |
|---|---|---|---|---|
shared_preferences | Key-value | No | Fast | App settings, small values |
hive | Key-value + boxes | Basic | Very fast | Non-relational cached data |
drift | SQL (SQLite) | Full SQL | Medium | Relational data, complex queries |
isar | NoSQL | Dart API | Very fast | Large datasets, complex indexes |
flutter_secure_storage | Key-value (encrypted) | No | Medium | Tokens, 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.