Flutter: Local Storage
Data without persistence is morning dew — gone when the app closes. Local storage gives data a home on the device.
📋 Prerequisites: You need to master the following first
- Lesson 12: State Management — Riverpod
1. What You Will Learn
- SharedPreferences: key-value storage (user settings, theme preferences, first-launch flag)
- Hive: NoSQL lightweight database, TypeAdapter custom object serialization
- sqflite / Drift: relational database, DAO pattern and migration strategy
- flutter_secure_storage: encrypted storage (tokens, keys)
- ShopApp: Hive product cache + SecureStorage for JWT + SharedPreferences for user preferences
2. A Story of Losing Data Offline
(1) Pain Point: Data Resets on Every Launch
Bob's ShopApp users report that every time they open the app, the cart is empty, favorited products are gone, and theme settings reset to default. This is because all data only exists in memory — closing the app means losing everything. Worse, the JWT Token is also stored in memory and gets lost when switching pages, forcing users to log in repeatedly.
(2) The Layered Storage Solution
Different data has different storage needs: tokens need encryption, product caches need structured storage, and user settings only need simple key-value pairs.
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';
// ⚙️ Install dependency: flutter pub add flutter_secure_storage hive hive_flutter shared_preferences
// Layered storage strategy
final token = await SecureStorage.getToken(); // Encrypted
final cached = await HiveBox.getProducts(); // NoSQL
final theme = await SharedPreferences.getTheme(); // KV
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
(3) Benefit: Offline Usable + Seamless Login
After implementing local storage, Bob gets encrypted token persistence for seamless login, product caching for offline browsing, and preferences that survive app restarts.
3. Storage Solution Selection
graph LR
subgraph Storage Selection
SP[SharedPreferences] --> |KV lightweight| Settings[User Settings]
HV[Hive] --> |NoSQL| Cache[Product Cache]
DR[Drift/SQLite] --> |SQL| Orders[Order History]
FSS[SecureStorage] --> |Encrypted| Token[JWT Token]
end
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
| Solution | Data Type | Encrypted | Capacity | Use Case |
|---|---|---|---|---|
| SharedPreferences | KV simple values | ❌ | Small | Settings, flags |
| Hive | NoSQL documents | Optional | Medium | Caches, objects |
| Drift/SQLite | SQL relational | ❌ | Large | Orders, history |
| SecureStorage | KV encrypted | ✅ | Small | Tokens, keys |
4. SharedPreferences
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: User preferences
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
// ⚙️ Install dependency: flutter pub add shared_preferences
class UserPreferences {
static const _keyTheme = 'theme_mode';
static const _keyLocale = 'locale';
static const _keyFirstLaunch = 'first_launch';
static const _keyCurrency = 'currency';
static Future<ThemeMode> getTheme() async {
final prefs = await SharedPreferences.getInstance();
final value = prefs.getString(_keyTheme);
return ThemeMode.values.firstWhere((m) => m.name == value, orElse: () => ThemeMode.system);
}
static Future<void> setTheme(ThemeMode mode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyTheme, mode.name);
}
static Future<bool> isFirstLaunch() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keyFirstLaunch) ?? true;
}
static Future<void> setFirstLaunchDone() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyFirstLaunch, false);
}
static Future<String> getCurrency() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_keyCurrency) ?? 'USD';
}
static Future<void> setCurrency(String currency) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyCurrency, currency);
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: First-launch onboarding page
import 'package:flutter/material.dart';
// Custom class definition source:
// - UserPreferences: see Lesson 4 SharedPreferences example in this lesson
class SplashPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
_checkFirstLaunch(context);
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
Future<void> _checkFirstLaunch(BuildContext context) async {
final isFirst = await UserPreferences.isFirstLaunch();
if (isFirst) {
Navigator.pushReplacementNamed(context, '/onboarding');
await UserPreferences.setFirstLaunchDone();
} else {
Navigator.pushReplacementNamed(context, '/home');
}
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
5. Hive NoSQL Database
(1) Hive Core Concepts
| Concept | Description |
|---|---|
| Hive | Database instance |
| Box | Similar to a table, stores key-value pairs |
| TypeAdapter | Custom object serialization/deserialization |
| HiveObject | Persistent object with auto-managed key |
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: Hive product cache
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
// ⚙️ Install dependency: flutter pub add hive hive_flutter
// ⚙️ Dev dependency: flutter pub add --dev hive_generator build_runner
// Custom class definition source:
// - Product: see Lesson 11 json_serializable model (simplified version below)
/*
class Product {
final int id;
final String name;
final double price;
final String imageUrl;
final String category;
const Product({required this.id, required this.name, required this.price,
required this.imageUrl, required this.category});
}
*/
// Model with Hive adapter
@HiveType(typeId: 0)
class ProductHive extends HiveObject {
@HiveField(0) late int id;
@HiveField(1) late String name;
@HiveField(2) late double price;
@HiveField(3) late String imageUrl;
@HiveField(4) late String category;
}
// Initialize Hive
Future<void> initHive() async {
await Hive.initFlutter();
Hive.registerAdapter(ProductHiveAdapter());
await Hive.openBox<ProductHive>('products');
await Hive.openBox('cart');
}
// Product cache repository
class ProductCache {
static const _boxName = 'products';
static Future<void> saveProducts(List<Product> products) async {
final box = Hive.box<ProductHive>(_boxName);
await box.clear();
for (final p in products) {
await box.put(p.id, ProductHive()
..id = p.id
..name = p.name
..price = p.price
..imageUrl = p.imageUrl
..category = p.category);
}
}
static Future<List<Product>> getProducts() async {
final box = Hive.box<ProductHive>(_boxName);
if (box.isEmpty) return [];
return box.values.map((h) => Product(
id: h.id, name: h.name, price: h.price,
imageUrl: h.imageUrl, category: h.category,
)).toList();
}
static Future<void> clearCache() async {
final box = Hive.box<ProductHive>(_boxName);
await box.clear();
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: Hive cart persistence
import 'package:hive/hive.dart';
// ⚙️ Install dependency: flutter pub add hive hive_flutter
// Custom class definition source:
// - CartItem: see Lesson 12 CartNotifier example
class CartStorage {
static const _boxName = 'cart';
static Future<void> saveCart(List<CartItem> items) async {
final box = Hive.box(_boxName);
await box.clear();
await box.put('items', items.map((i) => {
'product_id': i.product.id,
'quantity': i.quantity,
}).toList());
}
static Future<List<Map<String, dynamic>>> loadCart() async {
final box = Hive.box(_boxName);
final data = box.get('items');
return data != null ? List<Map<String, dynamic>>.from(data) : [];
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
6. Drift (SQLite) Relational Database
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: Drift order database
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
// ⚙️ Install dependency: flutter pub add drift drift_flutter sqlite3_flutter_libs
// ⚙️ Dev dependency: flutter pub add --dev drift_dev build_runner
// Table definition
class Orders extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get orderNumber => text().withLength(min: 8, max: 20)();
RealColumn get total => real()();
TextColumn get status => text().withDefault(const Constant('pending'))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get currency => text().withDefault(const Constant('USD'))();
}
class OrderItems extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get orderId => integer().references(Orders, #id)();
TextColumn get productName => text()();
RealColumn get price => real()();
IntColumn get quantity => integer()();
}
// Database class
@DriftDatabase(tables: [Orders, OrderItems])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(NativeDatabase.memory());
@override
int get schemaVersion => 1;
// Create order
Future<int> createOrder(OrdersCompanion order) =>
into(orders).insert(order);
// Get order with items
Future<List<OrderWithItems>> getOrderWithItems(int orderId) {
final query = select(orders).join([
leftOuterJoin(orderItems, orderItems.orderId.equalsExp(orders.id)),
])..where(orders.id.equals(orderId));
return query.map((row) {
final order = row.readTable(orders);
final item = row.readTableOrNull(orderItems);
return OrderWithItems(order: order, item: item);
}).toList();
}
// Get recent orders
Future<List<Order>> getRecentOrders() =>
(select(orders)..orderBy([(t) => OrderingTerm.desc(t.createdAt)])
..limit(20)).get();
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
7. Flutter Secure Storage
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: JWT Token encrypted storage
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
// ⚙️ Install dependency: flutter pub add flutter_secure_storage
class SecureStorage {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
static const _keyAccessToken = 'access_token';
static const _keyRefreshToken = 'refresh_token';
static const _keyUserId = 'user_id';
static Future<void> saveTokens({
required String accessToken,
required String refreshToken,
required String userId,
}) async {
await _storage.write(key: _keyAccessToken, value: accessToken);
await _storage.write(key: _keyRefreshToken, value: refreshToken);
await _storage.write(key: _keyUserId, value: userId);
}
static Future<String?> getAccessToken() =>
_storage.read(key: _keyAccessToken);
static Future<String?> getRefreshToken() =>
_storage.read(key: _keyRefreshToken);
static Future<void> clearAll() => _storage.deleteAll();
static Future<bool> isLoggedIn() async {
final token = await getAccessToken();
return token != null && token.isNotEmpty;
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
8. Complete Example: ShopApp Storage Layer
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
// ⚙️ Install dependency: flutter pub add flutter_riverpod riverpod_annotation shared_preferences
// ⚙️ Dev dependency: flutter pub add --dev riverpod_generator build_runner
// Custom class definition sources:
// - SecureStorage: see Lesson 7 Flutter Secure Storage in this lesson
// - Product: see Lesson 11 json_serializable model
// - User/AuthService: see Lesson 14 Auth Notifier
// - productRepositoryProvider: see Lesson 14 ProductRepository
// storage_provider.dart - Riverpod integration
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError('Override in main');
});
final secureStorageProvider = Provider<SecureStorage>((ref) => SecureStorage());
final productCacheProvider = Provider<ProductCache>((ref) => ProductCache());
// Auth state with persistent token
@riverpod
class Auth extends _$Auth {
@override
Future<User?> build() async {
final storage = ref.read(secureStorageProvider);
final token = await storage.getAccessToken();
if (token == null) return null;
// Validate token with API
try {
final user = await AuthService.validateToken(token);
return user;
} catch (_) {
await storage.clearAll();
return null;
}
}
Future<void> login(String email, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final result = await AuthService.login(email, password);
await ref.read(secureStorageProvider).saveTokens(
accessToken: result.accessToken,
refreshToken: result.refreshToken,
userId: result.user.id,
);
return result.user;
});
}
Future<void> logout() async {
await ref.read(secureStorageProvider).clearAll();
state = const AsyncData(null);
}
}
// Product with offline cache
@riverpod
class Products extends _$Products {
@override
Future<List<Product>> build() async {
final cache = ref.read(productCacheProvider);
// Try cache first
final cached = await cache.getProducts();
if (cached.isNotEmpty) return cached;
// Fallback to API
return _loadFromApi();
}
Future<List<Product>> _loadFromApi() async {
final repo = ref.read(productRepositoryProvider);
final products = await repo.getProducts();
await ref.read(productCacheProvider).saveProducts(products);
return products;
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => _loadFromApi());
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
❓ FAQ
onUpgrade callback for migrations. Hive uses box.deleteAndSaveFromStorage() or manual data conversion.📖 Summary
- SharedPreferences stores simple key-value pairs (settings/flags), not suitable for large data
- Hive NoSQL suits object caching, TypeAdapter supports custom serialization
- Drift (SQLite) suits relational data (orders/history), type-safe + migration support
- SecureStorage encrypts sensitive information (tokens/keys), Android uses EncryptedSharedPreferences
- Offline-first strategy: cache first + network fallback, Riverpod integration for seamless switching
📝 Exercises
- Basic (⭐): Use SharedPreferences to persist dark mode selection, keeping the choice after restarting the app.
- Intermediate (⭐⭐): Use Hive to persist the shopping cart locally — cart data survives closing and reopening the app.
- Challenge (⭐⭐⭐): Implement a complete offline cache strategy: read Hive cache first for display, fetch from API in the background, then refresh UI and cache after update.