This episode makes your data persist: local storage with shared_preferences and file I/O, local SQLite databases with sqflite and Drift, caching strategies and offline support, and secure storage for storing tokens and secrets safely.

Data that disappears when the app closes is a bad experience. Episode 9 teaches you how to store data persistently: lightweight preferences with shared_preferences, file I/O for binary data, relational SQLite databases through sqflite and Drift, caching strategies for offline support, and storing tokens and secrets with flutter_secure_storage.
Start by adding the base packages:
flutter pub add shared_preferences path_providerflutter pub add shared_preferences path_provider installs two packages at once. shared_preferences stores simple key-value pairs; path_provider provides platform-valid file directories.
Suitable for settings, small tokens, and UI flags:
final prefs = await SharedPreferences.getInstance();
await prefs.setString('nama_pengguna', 'Arman');
final nama = prefs.getString('nama_pengguna') ?? 'anonim';SharedPreferences.getInstance() gives a global instance. setString writes and getString reads values; use setInt, setBool, and setStringList for other types. Store only small data here — not large structures.
For documents, logs, or binary data, write to files:
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/catatan.txt');
await file.writeAsString('Isi catatan');
final isi = await file.readAsString();getApplicationDocumentsDirectory() returns the app's documents directory, valid on all platforms. The path is built with '${dir.path}/catatan.txt', then written and read through File.
When data needs queries, relations, and transactions, use SQLite:
flutter pub add sqflite pathHere's a simple table creation example:
final db = await openDatabase(
join(await getDatabasesPath(), 'app.db'),
version: 1,
onCreate: (db, version) async {
await db.execute(
'CREATE TABLE tasks(id INTEGER PRIMARY KEY, '
'title TEXT NOT NULL)',
);
},
);openDatabase opens or creates a database; onCreate runs the schema the first time the database is created. Note that path.join uses the path package to build correct paths across platforms.
Drift (formerly Moor) replaces raw SQL with compiler-checked queries. Its main benefit: query errors are caught at build time, not at runtime. For apps with complex schemas, Drift is worth considering.
Store API responses in storage and display them when offline:
Future<void> simpanCache(String key, String data) async {
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/cache_$key.json');
await file.writeAsString(data);
}
Future<String?> bacaCache(String key) async {
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/cache_$key.json');
if (await file.exists()) {
return file.readAsString();
}
return null;
}The simpanCache and bacaCache pattern lets the app show the last known data when the network is down, then refresh when back online. For more mature HTTP caching, the cached_network_image package handles images, and hive offers key-value object caching.
shared_preferences.cached_network_image.Choose the medium according to the shape of the data — storing everything in one place will only slow the app down.
Access tokens and secrets must never be stored as plain text:
flutter pub add flutter_secure_storageflutter_secure_storage uses the Android keystore and iOS Keychain — tokens are stored encrypted:
const storage = FlutterSecureStorage();
await storage.write(key: 'access_token', value: token);
final tersimpan = await storage.read(key: 'access_token');storage.write and storage.read use platform security. Never store tokens in shared_preferences or hardcode them in code — both are easy to read from a device.
Store only what's required in secure storage; store non-sensitive preferences in shared_preferences. For secrets that need to be synced across platforms or handled by the backend, consider a system-level keystore or a server-side secret manager — topics we'll touch on in episodes 14 and 19.
Key takeaways:
shared_preferences for light settings; path_provider for file I/O.sqflite for relational data; Drift as a type-safe alternative.flutter_secure_storage.In the next episode 10 we discuss advanced state management — comparing the Provider, Riverpod, Bloc, GetX, and MobX patterns, reactive state flow and dependency injection, managing complex state and modularization, and how to choose an architecture based on app size. This is the turning point from small apps to medium scale.