Local Storage
SharedPreferences, sqflite, Hive, flutter_secure_storage
- 01
Compare SharedPreferences, SQLite (sqflite), Hive, and flutter_secure_storage.
MediumPick by the shape of the data: key-value for settings, a database for records, the platform keystore for secrets.
// ── SharedPreferencesAsync — the current API ────── final prefs = SharedPreferencesAsync(); await prefs.setString('theme', 'dark'); await prefs.setBool('onboarding_done', true); final theme = await prefs.getString('theme') ?? 'light'; … - 02
What are the typical pitfalls of SharedPreferences and how do you avoid them?
EasySharedPreferences is the simplest store in Flutter, and every one of its sharp edges shows up in production sooner or later.
// ✅ Hydrate before runApp so UI never sees a stale default Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); final prefs = await SharedPreferencesWithCache.create( cacheOptions: const SharedPreferencesWithCacheOptions( allowList: {'theme', 'filters_v1', 'schema'}, … - 03
How do you handle schema migrations in sqflite?
Mediumsqflite drives migrations off the integer
versionyou pass toopenDatabase.Future<Database> openAppDb() async { return openDatabase( join(await getDatabasesPath(), 'app.db'), version: 3, onCreate: (db, v) async { // Apply ALL migrations from scratch … - 04
How do Hive type adapters and box encryption work?
MediumHive stores objects in "boxes" — append-only files keyed by string or int.
// Annotated model + generated adapter @HiveType(typeId: 1) class User extends HiveObject { @HiveField(0) String name; @HiveField(1) String email; @HiveField(2) DateTime createdAt; … - 05
How does flutter_secure_storage actually protect data on iOS vs Android?
Mediumflutter_secure_storage is a thin wrapper over the platform's own secure store, so what it actually guarantees differs per platform.
const _storage = FlutterSecureStorage( aOptions: AndroidOptions(), // ✅ v10+ default: RSA-OAEP + AES-GCM iOptions: IOSOptions( accessibility: KeychainAccessibility.first_unlock_this_device, ), ); … - 06
When should you read and write files directly with path_provider?
Easypath_providergives you platform-correct directories.import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; class NoteStore { … - 07
When would you choose Drift over raw sqflite?
MediumDrift (formerly Moor) is a typed wrapper over sqflite/sqlite3 with a code-gen layer.
// drift_dev generates everything below from this declaration @DriftDatabase(tables: [Tasks]) class AppDb extends _$AppDb { AppDb(super.e); @override int get schemaVersion => 2; … - 08
What are Isar and ObjectBox, and when do they beat sqflite?
MediumIsar and ObjectBox are NoSQL object databases written in native code (Rust for Isar, C++ for ObjectBox) with Dart bindings.
// Isar example (package: isar_community, the maintained v3 fork) // Annotated model + indexed lookup @collection class Note { Id id = Isar.autoIncrement; … - 09
What goes wrong with concurrent writes to a local database, and how do you avoid it?
HardA local database looks single-user, but sync workers, the UI and notification handlers all write to it concurrently.
// Pessimistic — wrap any multi-step write in a transaction Future<void> markDone(Database db, int id) { return db.transaction((txn) async { final rows = await txn.query('tasks', where: 'id = ?', whereArgs: [id], limit: 1); if (rows.isEmpty) throw StateError('not found'); … - 10
Users complain the app is eating gigabytes of storage. How do you find out where it went and keep it bounded?
MediumStorage bloat is nearly always a cache that nobody gave an eviction policy, so measure each directory first and then put a budget on every bucket.
Future<Map<String, int>> measureStorage() async { final dirs = { 'documents': await getApplicationDocumentsDirectory(), 'support': await getApplicationSupportDirectory(), 'cache': await getTemporaryDirectory(), }; … - 11
A sqflite query was instant with 500 rows and takes two seconds at 50 000. How do you diagnose and fix it?
HardRun EXPLAIN QUERY PLAN on the statement first, because the answer is usually that SQLite is scanning the table where it should be searching an index.
// 1. Ask SQLite what it is doing final plan = await db.rawQuery( 'EXPLAIN QUERY PLAN ' 'SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 20', [userId], ); … - 12
What has to be wiped when a user logs out, and what do teams usually forget?
MediumLogging out means destroying every copy of that user's data on the device, not just navigating back to the sign-in screen.
class LogoutService { LogoutService(this._api, this._db, this._secure, this._prefs, this._push); Future<void> logout() async { try { await _api.revokeSession(); // best effort, may fail offline … - 13
You need to see what your app actually wrote to disk on a test device — where do those files live and how do you open them?
EasyEvery store is a file inside the app sandbox, so the fastest way to see what you wrote is to pull that file onto your machine and open it with a normal desktop tool.
import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:sqflite/sqflite.dart'; … - 14
Your sqflite insert throws the moment you pass a DateTime and a bool — how should those columns be stored and read back?
Easysqflite only binds
num,String,Uint8Listandnull, because SQLite itself has exactly five storage classes — so every richer type needs an encoding you own on both sides.enum Priority { low, normal, high } class Task { Task({this.id, required this.title, required this.done, required this.dueAt, required this.priority}); … - 15
Users who set up a new phone from a backup say the app opens empty or crashes on the database — what did the restore actually do?
MediumBackups copy your files but never the platform key store, so an app that restores an encrypted database whose key lived in the Keychain or Keystore comes back with a file it can no longer read.
// sqflite_sqlcipher: the key must survive a restore, or the file is unreadable. const _secure = FlutterSecureStorage( iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), ); Future<Database> openStore() async { … - 16
Importing a 20 MB JSON export freezes the UI for four seconds even though every storage call is awaited — where is the time actually going?
Mediumawaityields the isolate, it does not move work off it: the SQL runs on a platform thread, but the JSON decode, the channel codec and the row-to-model loop are all Dart work on the UI isolate, and none of it can be interrupted to draw a frame.// ❌ awaited, and still four seconds of jank: every line is UI-isolate work Future<void> importSlow(Database db, String jsonText) async { final rows = (jsonDecode(jsonText) as List).cast<Map<String, Object?>>(); for (final row in rows) { await db.insert('items', row); // one codec encode + platform hop per row } … - 17
A list built from sqflite doesn't refresh when a background sync writes new rows — how do you make local storage reactive without polling?
Mediumsqflite has no change notification of any kind, so either every write goes through one object that also emits, or you move to a store that watches tables for you.
// ✅ one owner: every write goes through here, and every write emits class TaskRepository { TaskRepository(this._db); final Database _db; final _changes = StreamController<List<Task>>.broadcast(); … - 18
The key-value package your app is built on is unmaintained — how do you move a shipped app's local data to a new store without losing any of it?
MediumTreat it as a one-way data migration that must run at most once per install, resume after a kill, and be verified before you delete the old copy.
// One-way migration: at most once per install, resumable, verified. Future<void> migrateNotesToSql(Database db, Box<Note> box) async { final done = Sqflite.firstIntValue(await db.rawQuery( "SELECT value FROM meta WHERE key = 'notes_migrated'")); if (done == 1) return; … - 19
Your FCM background handler bumps a counter in SharedPreferences and Hive, but the UI only sees the new value after a restart — why?
HardA background handler runs in its own isolate with its own copy of everything on the Dart side, so any store that caches state in Dart memory ends up with two independent copies that go stale and overwrite each other.
@pragma('vm:entry-point') Future<void> onBackgroundMessage(RemoteMessage message) async { await Firebase.initializeApp(); // ❌ the UI isolate loaded its map at launch and will never see this write // final prefs = await SharedPreferences.getInstance(); … - 20
Crashlytics shows a steady trickle of "database disk image is malformed" — how do you find the cause and get those users working again?
HardSQLite almost never corrupts itself, so treat every one of those reports as something outside SQLite touching the file, and design the app so that losing the local database is an inconvenience rather than data loss.
Future<Database> openWithRecovery() async { // Application Support, not Caches: the OS may delete Caches while it is open. final path = p.join((await getApplicationSupportDirectory()).path, 'app.db'); try { final db = await openDatabase(path, version: 4, onCreate: _create, onUpgrade: _upgrade); …