Local Storage
Everything covered in the last two lessons — ChangeNotifier, Riverpod, Bloc — lives
entirely in memory. The moment the app process is killed, that state is gone. Most
real apps need at least some data to survive an app restart: a logged-in flag, a chosen theme, a
cached list of favorites, or a full offline dataset. Flutter doesn't bundle one single "save this"
API — instead, a small number of well-established packages each target a different shape of data.
This lesson covers the two you'll reach for most, plus a third worth knowing about.
shared_preferences: Simple Key-Value Storage
shared_preferences is the standard package for storing small, simple pieces of data as
key-value pairs — a username, a "dark mode enabled" flag, an onboarding-seen boolean, a saved sort
order. It is not designed for large amounts of data or anything relational; think
of it as a small persistent dictionary, backed by UserDefaults on iOS and
SharedPreferences on Android under the hood.
shared_preferences is the right tool. If you find yourself wanting to store a list of complex objects or query the data, that's a sign you actually need sqflite or hive instead.Saving a Value
Getting access to the storage instance is async — SharedPreferences.getInstance()
returns a Future because it has to actually read from the device's real storage layer
the first time it's called, which takes a small but non-zero amount of time. This is the exact same
async/await pattern covered in
HTTP Requests & APIs for network calls — reading from disk,
like reading from the network, is never instantaneous, so Dart models it as asynchronous work you
await rather than blocking the whole app while it happens.
Future<void> saveUsername(String name) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', name);
// Also available: setBool, setInt, setDouble, setStringList
await prefs.setBool('onboarding_complete', true);
}
Reading a Value Back
Reading uses the exact same getInstance() call, then a typed getter. Every getter
returns null if the key was never set, so it's good practice to always provide a
sensible fallback with the ?? operator.
Future<String> loadUsername() async {
final prefs = await SharedPreferences.getInstance();
// getString() returns null if 'username' was never saved
return prefs.getString('username') ?? 'Guest';
}
Future<void> clearUsername() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('username');
}
SharedPreferences.getInstance() caches its result internally after the first call within the same app run, so calling it repeatedly is cheap — you don't need to manually store the instance yourself, though many apps do anyway for convenience.sqflite: A Real Local SQLite Database
When your data is actually structured and relational — orders with line items, notes with tags,
anything you'd want to filter, sort, or join — shared_preferences stops being a good
fit. sqflite gives you a genuine local SQLite database, with real SQL
tables and queries, running entirely on the device. It's a bigger topic than
shared_preferences, so this section stays intentionally brief — enough to recognize
the shape of it.
Future<Database> openAppDatabase() async {
final databasesPath = await getDatabasesPath();
final path = join(databasesPath, 'app.db');
return openDatabase(
path,
version: 1,
onCreate: (db, version) async {
// Runs once, the very first time the database is created
await db.execute('''
CREATE TABLE notes(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
created_at TEXT NOT NULL
)
''');
},
);
}
Future<void> insertNote(Database db, String title) async {
await db.insert('notes', {
'title': title,
'created_at': DateTime.now().toIso8601String(),
});
}
Future<List<Map<String, Object?>>> fetchNotes(Database db) async {
// Returns rows as a List of Maps, newest first
return db.query('notes', orderBy: 'created_at DESC');
}
Every sqflite operation — opening the database, inserting, querying — is
async for the same underlying reason shared_preferences is: it's real disk
I/O. In a larger app, this raw SQL layer is usually wrapped by a repository class, or paired with
an ORM-style package, rather than called directly from widgets.
Choosing the Right Tool
| Situation | Use |
|---|---|
| A handful of simple settings, flags, or small strings | shared_preferences |
| Structured, relational data you need to query, filter, or sort | sqflite |
| Structured data, but you want speed without writing SQL | hive (see below) |
Where Hive Fits In
hive is a popular NoSQL local storage package worth knowing conceptually, even without
going deep here. It stores structured data — including whole Dart objects, via generated adapters —
without requiring any actual SQL at all, and its key-value-per-box design makes it notably faster
than sqflite for many common read/write patterns. It's a common choice when a team
wants something more capable than shared_preferences but wants to avoid writing and
maintaining raw SQL entirely.