Picking the wrong local storage option in Flutter means rewriting your data layer later — sometimes completely. It’s one of those architecture decisions that feels small at the start and becomes expensive if you get it wrong.
The good news: Flutter has exactly three mainstream options, and each has a clear use case. SharedPreferences for simple settings. Hive for offline object storage without SQL. sqflite for structured, relational data that needs querying. The hard part is knowing which situation you’re in — and that’s exactly what this post settles.
The original
hive and hive_flutter packages are no longer actively maintained by their original author. The community created hive_ce (Community Edition) as a maintained fork. If you’re starting a new project in 2026, use hive_ce — not the original hive. This post covers both where relevant, and all Hive code examples use the hive_ce API.
- Dart async/await and Future basics — all three options use async APIs
- Know how to add packages to pubspec.yaml
- Comfortable with StatefulWidget and initState — where you’ll load stored data
1. Quick Answer: The Comparison Table
If you’re in a hurry, this table gives you the answer. The rest of the post backs it up with code and explanation:
| Attribute | SharedPreferences | Hive (hive_ce) | sqflite |
|---|---|---|---|
| Storage model | Key-value (primitives only) | Key-value (any Dart object) | Relational SQL tables |
| Data types | int, double, bool, String, List<String> | Primitives + custom objects via TypeAdapter | Anything (stored as Maps) |
| Setup complexity | ⭐ Minimal | ⭐⭐ Moderate (code gen) | ⭐⭐⭐ Most involved |
| SQL required | No | No | Yes |
| Querying / filtering | None | Manual iteration | Full SQL WHERE / JOIN |
| Relationships | No | No | Yes (JOINs, foreign keys) |
| Encryption | No (use flutter_secure_storage) | Yes (AES-256 built-in) | No (use SQLCipher) |
| Platform support | All (incl. Web) | All (incl. Web, pure Dart) | Android/iOS/macOS + FFI for others |
| Performance | Fast (simple ops) | Very fast (cached reads) | Moderate |
| Maintenance | ✅ Official Flutter team | ⚠️ Community fork (hive_ce) | ✅ Actively maintained |
| Best for | App settings, flags, prefs | Caching, offline lists, objects | Complex structured data at scale |
2. Decision Flowchart — Pick in 30 Seconds
3. SharedPreferences: App Settings and Simple Flags
SharedPreferences wraps platform-native key-value storage: NSUserDefaults on iOS/macOS, SharedPreferences on Android, and LocalStorage on Web. It is published by the official Flutter team (flutter.dev) and has over 10,500 likes on pub.dev. It is the right tool for one specific job: storing small primitive values that represent user preferences or app state.
- You’re storing a dark mode preference (bool)
- You’re tracking whether the user has seen the onboarding screen (bool)
- You’re saving the selected language code (String)
- You’re storing a simple counter or score (int)
- You need the simplest possible setup with no boilerplate
Setup
# pubspec.yaml
dependencies:
shared_preferences: ^2.3.4 # check pub.dev for latest
The Three APIs (Important for 2026)
As of v2.3.0+, SharedPreferences has three distinct APIs. Most tutorials still show the legacy one — here’s what they all are and which to use:
import 'package:shared_preferences/shared_preferences.dart';
// ── API 1: Legacy (SharedPreferences.getInstance) ─────────────────────────
// ⚠️ Still works, but being deprecated. Most tutorials show this.
// Has a local in-memory cache — fast reads, but stale if another isolate
// (e.g. Firebase Messaging background handler) writes to the same store.
void legacyExample() async {
final prefs = await SharedPreferences.getInstance();
// Write
await prefs.setBool('isDarkMode', true);
await prefs.setString('username', 'Alice');
await prefs.setInt('loginCount', 5);
await prefs.setDouble('textScale', 1.2);
await prefs.setStringList('recentSearches', ['Flutter', 'Dart']);
// Read (synchronous — reads from in-memory cache)
final isDark = prefs.getBool('isDarkMode') ?? false;
final username = prefs.getString('username') ?? 'Guest';
final count = prefs.getInt('loginCount') ?? 0;
final searches = prefs.getStringList('recentSearches') ?? [];
// Delete
await prefs.remove('username');
await prefs.clear(); // remove ALL keys
print('Dark mode: $isDark, User: $username');
}
// ── API 2: SharedPreferencesAsync (new) ───────────────────────────────────
// ✅ Recommended when Firebase or background plugins share the same store.
// No local cache — every read goes to platform storage. Slightly slower.
void asyncApiExample() async {
const prefs = SharedPreferencesAsync();
await prefs.setString('theme', 'dark');
final theme = await prefs.getString('theme'); // always fresh from disk
await prefs.setBool('onboardingDone', true);
final done = await prefs.getBool('onboardingDone') ?? false;
print('Theme: $theme, Onboarding done: $done');
}
// ── API 3: SharedPreferencesWithCache (new, recommended default) ──────────
// ✅ Best of both — configurable allowList cache. Use this for most apps.
void withCacheExample() async {
final prefs = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(
allowList: {'theme', 'onboardingDone', 'loginCount'}, // only cache these keys
),
);
await prefs.setString('theme', 'light');
final theme = prefs.getString('theme') ?? 'light'; // reads from cache
await prefs.setBool('onboardingDone', false);
final done = prefs.getBool('onboardingDone') ?? false;
print('Theme: $theme, Onboarding: $done');
}
Loading in initState — The Standard Flutter Pattern
class _SettingsPageState extends State<SettingsPage> {
bool _isDarkMode = false;
String _language = 'en';
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadPreferences(); // load once on startup
}
Future<void> _loadPreferences() async {
const prefs = SharedPreferencesAsync();
final isDark = await prefs.getBool('isDarkMode') ?? false;
final lang = await prefs.getString('language') ?? 'en';
setState(() {
_isDarkMode = isDark;
_language = lang;
_isLoading = false;
});
}
Future<void> _toggleDarkMode(bool value) async {
const prefs = SharedPreferencesAsync();
await prefs.setBool('isDarkMode', value); // save to disk
setState(() => _isDarkMode = value); // update UI
}
@override
Widget build(BuildContext context) {
if (_isLoading) return const CircularProgressIndicator();
return SwitchListTile(
title: const Text('Dark Mode'),
value: _isDarkMode,
onChanged: _toggleDarkMode,
);
}
}
Auth tokens, passwords, API keys, and personal health or financial data must NOT go in SharedPreferences — it stores everything in plaintext and has no write-persistence guarantee. Use
flutter_secure_storage for sensitive values instead.
4. Hive (hive_ce): Offline Object Storage Without SQL
Hive is a lightweight, fast, pure-Dart NoSQL database that stores data in binary format. Unlike SharedPreferences it handles full Dart objects — lists, custom models, nested structures. Data is organised in Boxes (think: named containers). It requires no SQL knowledge and runs on every platform including Web.
The original
hive package is unmaintained. Add hive_ce and hive_ce_flutter to your project. The API is identical — it’s a drop-in replacement.
- You need to store custom Dart objects (notes, tasks, user profiles)
- You’re building a notes app, shopping cart, or chat message cache
- You want fast reads without writing SQL
- You need Web platform support
- You want built-in AES-256 encryption
- Your data doesn’t have complex relationships between entities
Setup
# pubspec.yaml
dependencies:
hive_ce: ^2.10.1 # community edition — use this, not 'hive'
hive_ce_flutter: ^2.2.0 # Flutter integration helpers
dev_dependencies:
hive_ce_generator: ^2.7.0 # code generation for TypeAdapters
build_runner: ^2.4.13
Primitive Storage (No TypeAdapter Needed)
import 'package:hive_ce_flutter/hive_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter(); // ← required BEFORE runApp
runApp(const MyApp());
}
// ── Storing primitives ─────────────────────────────────────────────────────
void primitiveExample() async {
// Open a box (like a named container)
final box = await Hive.openBox('settings');
// Write
await box.put('isDarkMode', true);
await box.put('username', 'Alice');
await box.put('loginCount', 5);
// Read
final isDark = box.get('isDarkMode', defaultValue: false) as bool;
final username = box.get('username', defaultValue: 'Guest') as String;
final count = box.get('loginCount', defaultValue: 0) as int;
print('Dark: $isDark, User: $username, Logins: $count');
// Delete
await box.delete('username');
await box.clear(); // clear all entries in this box
}
Storing Custom Objects with TypeAdapter
To store custom Dart classes, annotate the class and run code generation. The generator creates a TypeAdapter that Hive uses to serialise/deserialise your object:
// note.dart
import 'package:hive_ce/hive_ce.dart';
part 'note.g.dart'; // generated file — created by build_runner
@HiveType(typeId: 0) // unique ID for this type across your whole app
class Note extends HiveObject { // HiveObject gives you .save() and .delete()
@HiveField(0)
late String title;
@HiveField(1)
late String body;
@HiveField(2)
late DateTime createdAt;
@HiveField(3)
late bool isPinned;
Note({
required this.title,
required this.body,
required this.createdAt,
this.isPinned = false,
});
}
// Generate the adapter:
// flutter pub run build_runner build --delete-conflicting-outputs
// This creates note.g.dart containing NoteAdapter
// main.dart — register the adapter BEFORE opening boxes
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
Hive.registerAdapter(NoteAdapter()); // ← register generated adapter
runApp(const MyApp());
}
// ── CRUD operations on a Hive box of Notes ─────────────────────────────────
class HiveNotesService {
static const String _boxName = 'notes';
// Open the box (call once, keep the reference)
Future<Box<Note>> _openBox() => Hive.openBox<Note>(_boxName);
// CREATE — add a note
Future<void> addNote(Note note) async {
final box = await _openBox();
await box.add(note); // auto-increments the key
}
// READ — get all notes
Future<List<Note>> getAllNotes() async {
final box = await _openBox();
return box.values.toList(); // returns all Note objects
}
// READ — get pinned notes only (manual filtering)
Future<List<Note>> getPinnedNotes() async {
final box = await _openBox();
return box.values.where((note) => note.isPinned).toList();
}
// UPDATE — using HiveObject.save() (requires extending HiveObject)
Future<void> updateNote(Note note) async {
note.title = 'Updated title';
await note.save(); // saves changes back to the box automatically
}
// DELETE — using HiveObject.delete()
Future<void> deleteNote(Note note) async {
await note.delete(); // removes from box
}
// DELETE by key
Future<void> deleteNoteByKey(int key) async {
final box = await _openBox();
await box.delete(key);
}
// Count
Future<int> getNoteCount() async {
final box = await _openBox();
return box.length;
}
}
// ── Using the service in a StatefulWidget ──────────────────────────────────
class _NotesPageState extends State<NotesPage> {
final _service = HiveNotesService();
List<Note> _notes = [];
@override
void initState() {
super.initState();
_loadNotes();
}
Future<void> _loadNotes() async {
final notes = await _service.getAllNotes();
setState(() => _notes = notes);
}
Future<void> _addNote() async {
await _service.addNote(Note(
title: 'New Note',
body: 'Tap to edit',
createdAt: DateTime.now(),
));
_loadNotes(); // refresh the list
}
}
Encrypted Box (AES-256)
// Encrypted box — store the key securely (e.g. using flutter_secure_storage)
Future<void> openEncryptedBox() async {
// Generate a random 256-bit key (do this once, store it securely)
final encryptionKey = Hive.generateSecureKey(); // List<int> of 32 bytes
// Open an encrypted box
final encryptedBox = await Hive.openBox(
'sensitiveData',
encryptionCipher: HiveAesCipher(encryptionKey),
);
await encryptedBox.put('apiToken', 'my-secret-token');
// Data is stored as AES-256 encrypted binary — unreadable without the key
}
5. sqflite: Structured Relational Data
sqflite is a Flutter plugin wrapping SQLite — the industry-standard embedded relational database that’s been around for over 20 years. It supports full SQL: CREATE TABLE, INSERT, SELECT, WHERE, JOIN, transactions, and schema migrations. Use it when your data has structure and relationships.
- Your data has relationships (users → orders, students → attendance records)
- You need complex filtering, sorting, or JOINs
- Your dataset could grow to thousands of rows
- You or your team already knows SQL
- You need transactions (e.g. transfer money between accounts atomically)
- You’re building an inventory, finance, or record-keeping app
Setup
# pubspec.yaml
dependencies:
sqflite: ^2.4.2
path: ^1.9.0 # for constructing the database file path
The DatabaseHelper Singleton (Critical Pattern)
sqflite requires careful initialisation. The standard pattern uses a singleton helper class with a static Database? instance so the database file is only opened once, not on every query. Getting this wrong causes performance issues and occasional corruption:
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
// ── Singleton pattern ─────────────────────────────────────────────────
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
static Database? _database; // null until first access
// Returns the open database, opening it on first call
Future<Database> get database async {
if (_database != null) return _database!; // already open
_database = await _initDatabase(); // open once
return _database!;
}
Future<Database> _initDatabase() async {
// Get the path to the database file
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'app.db');
return await openDatabase(
path,
version: 1,
onCreate: _createTables, // called when DB is first created
onUpgrade: _onUpgrade, // called when version increases
);
}
// Create your tables here
Future<void> _createTables(Database db, int version) async {
await db.execute('''
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT,
isDone INTEGER NOT NULL DEFAULT 0,
created TEXT NOT NULL
)
''');
await db.execute('''
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)
''');
}
// Migrate schema when version increases
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Version 2 adds a priority column
await db.execute('ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 0');
}
}
}
Model Class with toMap / fromMap
class Task {
final int? id; // null for new tasks (DB assigns the id on insert)
final String title;
final String? body;
final bool isDone;
final DateTime created;
const Task({
this.id,
required this.title,
this.body,
this.isDone = false,
required this.created,
});
// Convert to Map for sqflite insert/update
Map<String, dynamic> toMap() => {
if (id != null) 'id': id,
'title': title,
'body': body,
'isDone': isDone ? 1 : 0, // SQLite has no bool type — use 0/1
'created': created.toIso8601String(),
};
// Create a Task from a Map returned by sqflite query
factory Task.fromMap(Map<String, dynamic> map) => Task(
id: map['id'] as int,
title: map['title'] as String,
body: map['body'] as String?,
isDone: (map['isDone'] as int) == 1, // convert 0/1 back to bool
created: DateTime.parse(map['created'] as String),
);
// copyWith: create a modified copy (useful for updates)
Task copyWith({int? id, String? title, String? body, bool? isDone}) => Task(
id: id ?? this.id,
title: title ?? this.title,
body: body ?? this.body,
isDone: isDone ?? this.isDone,
created: created,
);
}
Full CRUD Operations
class TaskRepository {
final _db = DatabaseHelper.instance;
// ── CREATE ────────────────────────────────────────────────────────────
Future<int> insertTask(Task task) async {
final db = await _db.database;
return await db.insert(
'tasks',
task.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
// Returns the new row's id
}
// ── READ all ──────────────────────────────────────────────────────────
Future<List<Task>> getAllTasks() async {
final db = await _db.database;
final rows = await db.query(
'tasks',
orderBy: 'created DESC', // newest first
);
return rows.map(Task.fromMap).toList();
}
// ── READ with filter ──────────────────────────────────────────────────
Future<List<Task>> getIncompleteTasks() async {
final db = await _db.database;
final rows = await db.query(
'tasks',
where: 'isDone = ?', // parameterised query — prevents SQL injection
whereArgs: [0],
orderBy: 'created ASC',
);
return rows.map(Task.fromMap).toList();
}
// ── READ by ID ────────────────────────────────────────────────────────
Future<Task?> getTaskById(int id) async {
final db = await _db.database;
final rows = await db.query(
'tasks',
where: 'id = ?',
whereArgs: [id],
limit: 1,
);
if (rows.isEmpty) return null;
return Task.fromMap(rows.first);
}
// ── UPDATE ────────────────────────────────────────────────────────────
Future<int> updateTask(Task task) async {
final db = await _db.database;
return await db.update(
'tasks',
task.toMap(),
where: 'id = ?',
whereArgs: [task.id],
);
// Returns the number of rows affected
}
// ── DELETE ────────────────────────────────────────────────────────────
Future<int> deleteTask(int id) async {
final db = await _db.database;
return await db.delete(
'tasks',
where: 'id = ?',
whereArgs: [id],
);
}
// ── SEARCH ────────────────────────────────────────────────────────────
Future<List<Task>> searchTasks(String query) async {
final db = await _db.database;
final rows = await db.query(
'tasks',
where: 'title LIKE ? OR body LIKE ?',
whereArgs: ['%$query%', '%$query%'], // % = wildcard
);
return rows.map(Task.fromMap).toList();
}
// ── RAW SQL for complex queries ────────────────────────────────────────
Future<int> getCompletedCount() async {
final db = await _db.database;
final result = await db.rawQuery(
'SELECT COUNT(*) as count FROM tasks WHERE isDone = 1',
);
return result.first['count'] as int;
}
// ── TRANSACTION: multiple operations atomically ────────────────────────
Future<void> completeBatch(List<int> taskIds) async {
final db = await _db.database;
await db.transaction((txn) async {
for (final id in taskIds) {
await txn.update(
'tasks',
{'isDone': 1},
where: 'id = ?',
whereArgs: [id],
);
}
// If any update throws, ALL are rolled back automatically
});
}
}
JOIN Example: Relational Data
// Example: get tasks with their category names via a JOIN
// (Assumes tasks table has a category_id column)
Future<List<Map<String, dynamic>>> getTasksWithCategories() async {
final db = await DatabaseHelper.instance.database;
return await db.rawQuery('''
SELECT
tasks.id,
tasks.title,
tasks.isDone,
categories.name AS categoryName
FROM tasks
INNER JOIN categories ON tasks.category_id = categories.id
WHERE tasks.isDone = 0
ORDER BY categories.name, tasks.title
''');
// Returns List<Map> — parse into your model as needed
}
6. Honorable Mentions
Before picking one of the three main options, here are four additional packages that come up frequently and are worth knowing about:
| Package | What it is | Use when |
|---|---|---|
flutter_secure_storage | Encrypted key-value storage (Keychain on iOS, Keystore on Android) | Storing auth tokens, passwords, API keys — anything sensitive |
| Drift (formerly Moor) | Type-safe ORM layer on top of SQLite with reactive streams | You want sqflite’s power without raw SQL strings — type errors caught at compile time |
hive_ce | Community Edition fork of Hive — actively maintained | Every new project that would have used Hive — use this instead of the original |
| ObjectBox / Isar | High-performance NoSQL databases | Proceed with caution — ObjectBox is not open source, Isar is also community-abandoned in 2025 |
Sometimes no local database is needed at all. If your data is purely session-scoped (doesn’t need to survive an app restart), just use a Dart
List in state with setState or a ChangeNotifier. Don’t add a database dependency if the problem doesn’t require one.
7. Common Mistakes
Mistake 1: Storing sensitive data in SharedPreferences
// ❌ Wrong — plaintext storage, no write-persistence guarantee
await prefs.setString('authToken', 'Bearer eyJhb...');
await prefs.setString('password', 'hunter2');
// ✅ Correct — use flutter_secure_storage for sensitive values
const storage = FlutterSecureStorage();
await storage.write(key: 'authToken', value: 'Bearer eyJhb...');
final token = await storage.read(key: 'authToken');
Mistake 2: Storing JSON-encoded objects in SharedPreferences
// ❌ Works but becomes unmaintainable — you're reimplementing a database
await prefs.setString('user', jsonEncode({'name': 'Alice', 'role': 'admin'}));
await prefs.setString('cart', jsonEncode(cartItems.map((i) => i.toJson()).toList()));
// When you have 5 of these, schema changes become painful
// ✅ Correct — if you're storing objects, use Hive (hive_ce)
final box = await Hive.openBox<User>('users');
await box.put('currentUser', alice); // typed, versioned via TypeAdapter
Mistake 3: Opening the sqflite database on every query
// ❌ Wrong — opens the database file every time; slow and risks file locks
Future<List<Task>> getTasks() async {
final db = await openDatabase('app.db'); // ← re-opens on every call
return (await db.query('tasks')).map(Task.fromMap).toList();
}
// ✅ Correct — use the singleton DatabaseHelper shown above
// The database is opened once and reused for every subsequent call
Future<List<Task>> getTasks() async {
final db = await DatabaseHelper.instance.database; // ← reuses open instance
return (await db.query('tasks')).map(Task.fromMap).toList();
}
Mistake 4: Using the original hive package in 2026
# ❌ Wrong — unmaintained, no new fixes or Flutter SDK updates
dependencies:
hive: ^2.2.3
hive_flutter: ^1.1.0
# ✅ Correct — community fork, actively maintained, drop-in replacement
dependencies:
hive_ce: ^2.10.1
hive_ce_flutter: ^2.2.0
dev_dependencies:
hive_ce_generator: ^2.7.0
build_runner: ^2.4.13
Mistake 5: Forgetting Hive.initFlutter() before runApp
// ❌ Wrong — opening a box before Hive is initialised throws:
// "HiveError: You need to initialize Hive or provide a path to store the box"
void main() {
runApp(const MyApp()); // Hive never initialised!
}
// ✅ Correct — init first, register adapters, then runApp
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // ← required for async in main
await Hive.initFlutter(); // ← init Hive
Hive.registerAdapter(NoteAdapter()); // ← register all adapters
Hive.registerAdapter(TaskAdapter()); // ← one line per custom type
runApp(const MyApp()); // ← then run the app
}
Mistake 6: Using sqflite for simple user preferences
// ❌ Overkill — 50+ lines of boilerplate to store a bool
// CREATE TABLE, DatabaseHelper, toMap, fromMap, all for isDarkMode = true
Future<void> saveDarkMode(bool isDark) async {
final db = await DatabaseHelper.instance.database;
await db.insert('settings', {'key': 'isDark', 'value': isDark ? 1 : 0},
conflictAlgorithm: ConflictAlgorithm.replace);
}
// ✅ Correct — 2 lines with SharedPreferences
const prefs = SharedPreferencesAsync();
await prefs.setBool('isDarkMode', isDark);
8. Interview Q&A
A: SharedPreferences stores only primitive key-value pairs (int, bool, String, double, List<String>) — perfect for app settings and flags, but can’t store objects. Hive stores any Dart object in a fast binary format using TypeAdapters, making it ideal for offline lists and object caching without SQL. sqflite wraps SQLite and provides full relational database capabilities with SQL queries, JOINs, and transactions — required when your data has relationships or needs complex filtering.
A: Hive (using
hive_ce) is the right choice for a notes app. Notes are self-contained objects with no relationships between them. Hive handles custom Dart objects natively via TypeAdapters, is faster than sqflite for simple reads and writes, and requires no SQL knowledge. sqflite would be overkill unless you need to JOIN notes to categories or users, or need full-text search across thousands of records.
A: No. SharedPreferences stores data in plaintext on disk with no encryption. Sensitive values like auth tokens, passwords, and API keys must be stored using
flutter_secure_storage, which uses iOS Keychain and Android Keystore — the platform’s encrypted credential stores.
A: A TypeAdapter is a class that tells Hive how to serialise and deserialise a custom Dart object to and from binary storage. You annotate your class with
@HiveType and its fields with @HiveField, then run build_runner to auto-generate the adapter. Without a TypeAdapter, Hive can only store primitive values — with one registered, it can store any Dart object directly in a Box.
A: SQLite database files can only be safely accessed by one open connection at a time within a Dart isolate. If you call
openDatabase() on every query, you re-open the file repeatedly — causing overhead and risking file lock errors. The singleton pattern ensures the file is opened exactly once and the same Database instance is reused for every subsequent operation.
9. Related Posts
| Post | Why it’s relevant |
|---|---|
| Notes App Part 2: Local Persistence with SharedPreferences | See SharedPreferences in action in a real project — the Notes app uses it for note persistence. |
| Flutter Dark Mode Toggle | The dark mode post uses SharedPreferences to persist the theme choice — a textbook use case for this guide. |
| Understanding pubspec.yaml | Adding shared_preferences, hive_ce, or sqflite all start with editing pubspec.yaml correctly. |
| Dart Crash Course (Part 0) | All three storage options use async/await — the Dart crash course covers exactly the language features needed here. |
| Flutter Widgets: Stateless vs Stateful | Loading stored data in initState is the standard pattern — requires understanding StatefulWidget’s lifecycle. |

Pingback: Flutter SharedPreferences Tutorial: Save User Data Without a Database