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

1. What You Will Learn


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.

DART
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
TEXT
> 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

100%
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
TEXT
> 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

TEXT
> 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

DART
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);
  }
}
TEXT
> 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

TEXT
> 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

DART
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');
    }
  }
}
TEXT
> 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

TEXT
> 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

DART
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();
  }
}
TEXT
> 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

TEXT
> 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

DART
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) : [];
  }
}
TEXT
> 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

TEXT
> 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

DART
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();
}
TEXT
> 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

TEXT
> 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

DART
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;
  }
}
TEXT
> 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

DART
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());
  }
}
TEXT
> 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

Q What happens if SharedPreferences stores large amounts of data?
A SharedPreferences loads everything into memory at once, so storing large data wastes memory. Use Hive or Drift for large data.
Q How to choose between Hive and Drift?
A Hive is for caching and unstructured data (NoSQL, simple and fast); Drift is for structured data requiring complex queries and relationships (SQL, type-safe).
Q Is SecureStorage secure on the Web?
A Web uses localStorage, which isn't truly encrypted. Sensitive data on the Web should be managed via backend sessions.
Q What if Hive's typeId is duplicated?
A typeId must be globally unique — duplicates cause deserialization errors. Maintain a typeId allocation table.
Q How to handle database migration (schema version upgrade)?
A Drift uses the onUpgrade callback for migrations. Hive uses box.deleteAndSaveFromStorage() or manual data conversion.
Q What's the relationship between Hive and Isar?
A Isar is the next-generation product from the Hive author, with better performance but a different API. Hive is still widely used and stable.

📖 Summary


📝 Exercises

  1. Basic (⭐): Use SharedPreferences to persist dark mode selection, keeping the choice after restarting the app.
  2. Intermediate (⭐⭐): Use Hive to persist the shopping cart locally — cart data survives closing and reopening the app.
  3. 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.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏