Irbisa · Cheatsheet

Local Storage

SharedPreferences, sqflite, Hive, flutter_secure_storage

Middle Developer · 25 questions

  1. 01

    When would you use SharedPreferences, sqflite, Hive or flutter_secure_storage?

    Pick by the shape of the data: key-value for settings, a real database for records, the platform keystore for secrets.

  2. 02

    How do you open a sqflite database and do basic CRUD on it?

    Open the file once with openDatabase (the path from getDatabasesPath()), create the tables in onCreate, then use insert, query, update and delete; values go in a map or as ? placeholders in where, never through string interpolation.

    Future<Database> openAppDb() async => openDatabase(      join(await getDatabasesPath(), 'app.db'), // join from package:path      version: 1,      onCreate: (db, version) => db.execute(        'CREATE TABLE notes(id INTEGER PRIMARY KEY AUTOINCREMENT, '        'title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0)',…
  3. 03

    What are the typical pitfalls of SharedPreferences and how do you avoid them?

    SharedPreferences is a small, untyped, non-reactive and unencrypted key-value file: load it before the UI needs it, keep only small settings in it, and never put secrets there.

    Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();  // Load once before the first frame, so the UI never flashes a default theme.  final prefs = await SharedPreferencesWithCache.create(    cacheOptions: const SharedPreferencesWithCacheOptions(      allowList: {'theme', 'filters'},…
  4. 04

    What is path_provider, and which directory should a file go to: documents, application support, temporary or cache?

    path_provider returns the platform's directories for your app; pick one by asking whether the file is user content, app data, or something you can recreate.

    Future<void> saveFiles(Note note, List<int> thumbBytes) async {  // user content: backed up, the user would miss it  final docs = await getApplicationDocumentsDirectory();  await File(p.join(docs.path, '${note.id}.json'))      .writeAsString(jsonEncode(note.toJson()));…
  5. 05

    Your sqflite insert throws as soon as you pass a DateTime: how should dates, bools and enums be stored and read back?

    sqflite binds only num, String, Uint8List and null, because SQLite stores just numbers, text and bytes; convert everything else in the model's toMap/fromMap (or a drift TypeConverter), never at call sites.

    enum Priority { low, normal, high } class Task {  Task({this.id, required this.title, required this.done,      required this.dueAt, required this.priority});  final int? id;…
  6. 06

    What is the difference between legacy SharedPreferences, SharedPreferencesAsync and SharedPreferencesWithCache?

    They differ in where the truth lives: legacy SharedPreferences caches every key in Dart, SharedPreferencesAsync asks the platform on every call, and SharedPreferencesWithCache caches only the keys you allow.

    import 'package:shared_preferences/shared_preferences.dart';import 'package:shared_preferences/util/legacy_to_async_migration_util.dart'; Future<SharedPreferencesWithCache> initPrefs() async {  const options = SharedPreferencesOptions();  // One-time copy of keys the legacy API wrote (a different backend on Android).…
  7. 07

    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?

    Every store is just a file inside the app sandbox, so pull the file onto your machine and open it with a desktop tool.

    # Android, debuggable build: copy the database together with its WAL fileadb exec-out run-as com.example.app cat databases/app.db > app.dbadb exec-out run-as com.example.app cat databases/app.db-wal > app.db-wal # Preferences written by the legacy SharedPreferences APIadb exec-out run-as com.example.app cat shared_prefs/FlutterSharedPreferences.xml…
  8. 08

    How do you handle schema migrations in sqflite?

    Bump the version you pass to openDatabase and bring old files forward in onUpgrade with one if (from < N) step per version; onCreate must build the latest schema directly.

    Future<Database> openAppDb() async => openDatabase(      join(await getDatabasesPath(), 'app.db'),      version: 3,      // Fresh install: create the latest schema directly.      onCreate: (db, version) => db.execute(        'CREATE TABLE tasks(id INTEGER PRIMARY KEY, title TEXT NOT NULL, '…
  9. 09

    Release 2 adds a due_at column. Updated users are fine, but fresh installs crash with no such column: due_at. Why?

    Future<Database> openDb(String path) => openDatabase(  path,  version: 2,  onCreate: (db, version) async {    await db.execute(        'CREATE TABLE tasks(id INTEGER PRIMARY KEY, title TEXT NOT NULL)');  },  onUpgrade: (db, oldVersion, newVersion) async {    if (oldVersion < 2) {      await db.execute('ALTER TABLE tasks ADD COLUMN due_at INTEGER');    }  },);

    On a fresh install sqflite calls only onCreate, never onUpgrade, and then stamps the file as version 2.

  10. 10

    How do Hive type adapters and box encryption work?

    Hive keeps objects in boxes: files whose keys are strings or ints.

    @HiveType(typeId: 1)class User extends HiveObject {  User({required this.name, required this.email});   @HiveField(0)  String name;…
  11. 11

    How does flutter_secure_storage actually protect data on iOS and on Android?

    It is a thin wrapper over the platform's own secure store, so the real guarantee is the platform's: the Keychain on iOS, a Keystore key on Android.

  12. 12

    When would you choose Drift over raw sqflite?

    Choose Drift when you want compile-checked queries, streams that re-emit when tables change and testable migrations; stay on raw sqflite for a handful of queries.

    class Tasks extends Table {  IntColumn get id => integer().autoIncrement()();  TextColumn get title => text().withLength(min: 1, max: 200)();  BoolColumn get done => boolean().withDefault(const Constant(false))();  DateTimeColumn get dueAt => dateTime().nullable()();}…
  13. 13

    What are Isar and ObjectBox, and when do they beat sqflite?

    Isar and ObjectBox are NoSQL object databases written in native code with Dart bindings: you store Dart objects directly instead of mapping them onto tables and rows.

  14. 14

    This sqflite transaction never completes and the log warns that the database has been locked: what is wrong?

    Future<void> moveTask(Database db, int taskId, int listId) async {  await db.transaction((txn) async {    await txn.update('tasks', {'list_id': listId},        where: 'id = ?', whereArgs: [taskId]);    final count = Sqflite.firstIntValue(await db.rawQuery(        'SELECT COUNT(*) FROM tasks WHERE list_id = ?', [listId]));    await txn.update('lists', {'task_count': count},        where: 'id = ?', whereArgs: [listId]);  });}

    Inside the transaction it calls db.rawQuery instead of txn.rawQuery.

  15. 15

    Find the bug: this NotesDao passes its unit tests, but on a screen that loads and saves at once some calls fail with a DatabaseException.

    class NotesDao {  NotesDao(this.path);  final String path;   Future<T> _use<T>(Future<T> Function(Database db) body) async {    final db = await openDatabase(path);    try {      return await body(db);    } finally {      await db.close();    }  }   Future<List<Map<String, Object?>>> all() => _use((db) => db.query('notes'));  Future<int> add(String text) => _use((db) => db.insert('notes', {'text': text}));}

    openDatabase returns one shared Database per path, so the close() in one call's finally closes it under every other call that is in flight and has not yet reached the database; those calls fail with a database-closed DatabaseException (calls that start later simply reopen it).

  16. 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?

    await only yields between pieces of work, it does not move them off the isolate: the freeze is synchronous Dart code, the jsonDecode of 20 MB and the loop that builds models.

    // wrong: jsonDecode blocks the UI isolate, plus one hop per rowFuture<void> importSlow(Database db, String json) async {  final rows = (jsonDecode(json) as List).cast<Map<String, Object?>>();  for (final row in rows) {    await db.insert('items', row);  }…
  17. 17

    Users complain the app is eating gigabytes of storage. How do you find out where it went and keep it bounded?

    Storage bloat is almost always a cache nobody gave an eviction rule.

    Future<Map<String, int>> measureStorage() async {  final dirs = {    'documents': await getApplicationDocumentsDirectory(),    'support': await getApplicationSupportDirectory(),    'cache': await getApplicationCacheDirectory(),  };…
  18. 18

    A list built from sqflite doesn't refresh when a background sync writes new rows: how do you make local storage reactive without polling?

    sqflite never tells anyone that something changed, so either every write goes through one object that also emits, or you move to a store that watches tables for you.

    class TaskRepository {  TaskRepository(this._db);  final Database _db;  final _changes = StreamController<List<Task>>.broadcast();   Stream<List<Task>> watchOpen() async* {…
  19. 19

    What has to be wiped when a user logs out, and what do teams usually forget?

    Logging out means destroying every copy of that user's data on the device, not just navigating to the sign-in screen.

  20. 20

    What goes wrong with concurrent writes to a local database, and how do you avoid it?

    A local database looks single-user, but the UI, a sync worker and a notification handler all write to it at the same time, often from different isolates.

    // Optimistic: the update lands only if nobody changed the row since we read it.Future<bool> rename(Database db, int id, String title, int readRevision) async {  final changed = await db.update(    'tasks',    {'title': title, 'revision': readRevision + 1},    where: 'id = ? AND revision = ?',…
  21. 21

    A sqflite query was instant with 500 rows and takes two seconds at 50 000. How do you diagnose and fix it?

    Run EXPLAIN QUERY PLAN on the statement first: at that size the answer is nearly always that SQLite is scanning the table where it should be searching an index.

    // 1. Ask SQLite what it does with the slow query.final plan = await db.rawQuery(  'EXPLAIN QUERY PLAN SELECT * FROM orders '  'WHERE user_id = ? ORDER BY created_at DESC LIMIT 20',  [userId],);…
  22. 22

    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?

    A restore brings back your files but not device-bound keys (Android Keystore keys never leave the device), so the new phone gets an encrypted database whose key no longer exists.

    <!-- res/xml/data_extraction_rules.xml (Android 12+), referenced by     android:dataExtractionRules in AndroidManifest.xml.     Android 11 and lower read android:fullBackupContent instead. --><data-extraction-rules>  <cloud-backup>    <!-- Encrypted with a Keystore key that never leaves the device -->…
  23. 23

    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?

    Treat it as a one-way data migration that runs at most once per install, survives the app being killed, and is verified before you delete the old copy.

  24. 24

    Your FCM background handler bumps a counter in SharedPreferences and Hive, but the UI only sees the new value after a restart: why?

    A background handler runs in its own isolate with its own Dart memory, so any store that caches state in Dart ends up with two copies that go stale and overwrite each other.

    @pragma('vm:entry-point')Future<void> onBackgroundMessage(RemoteMessage message) async {  // wrong: the UI isolate cached its map at launch and never sees this write  // final prefs = await SharedPreferences.getInstance();   // right: no Dart-side cache, every call goes to the platform store…
  25. 25

    Crashlytics shows a steady trickle of "database disk image is malformed": how do you find the cause and get those users working again?

    SQLite almost never corrupts itself, so read every one of those reports as something outside SQLite touching the file, and design so that losing the local database is an inconvenience, not data loss.