Learn Flutter - Persistent Storage
Episode 9 of 23

Learn Flutter - Persistent Storage

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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:

Add storage packages
flutter pub add shared_preferences path_provider

flutter 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.

Local Storage: shared_preferences and File I/O

shared_preferences for Light Preferences

Suitable for settings, small tokens, and UI flags:

Store and read preferences
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.

File I/O with path_provider

For documents, logs, or binary data, write to files:

Write a file in the documents directory
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.

Local Databases: SQLite / sqflite and Drift

sqflite for Relational Queries

When data needs queries, relations, and transactions, use SQLite:

Add sqflite and path
flutter pub add sqflite path

Here's a simple table creation example:

Create a database with sqflite
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 as a Type-safe Alternative

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.

Caching Strategies and Offline Support

Cache-First Pattern

Store API responses in storage and display them when offline:

Cache the latest data to a file
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.

Caching Priorities

  • Flags and settings: shared_preferences.
  • Images and assets: cached_network_image.
  • API responses: file cache or a local database.
  • Complex relational data: sqflite or Drift.

Choose the medium according to the shape of the data — storing everything in one place will only slow the app down.

Secure Storage for Secrets

flutter_secure_storage for Tokens

Access tokens and secrets must never be stored as plain text:

Add secure storage
flutter pub add flutter_secure_storage

flutter_secure_storage uses the Android keystore and iOS Keychain — tokens are stored encrypted:

Store a token securely
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.

Principles of Secret Storage

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.

Conclusion

Key takeaways:

  • shared_preferences for light settings; path_provider for file I/O.
  • SQLite via sqflite for relational data; Drift as a type-safe alternative.
  • Cache API responses to files so the app works offline.
  • Choose the storage medium based on the shape and sensitivity of the data.
  • Tokens and secrets must use flutter_secure_storage.
  • Never store secrets in code or plain-text preferences.

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.