Flutter: 本地存储

没有持久化的数据是朝露——应用一关,一切蒸发。本地存储让数据在设备上安家。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 一个离线丢失数据的故事

(1) 痛点:每次启动数据归零

Bob 的 ShopApp 用户反馈:每次打开应用购物车都是空的,已收藏的商品不见了,主题设置每次重置为默认。这是因为所有数据只存在内存中,应用关闭即丢失。更严重的是,JWT Token 也存在内存中,切换页面就丢失,用户被迫反复登录。

(2) 本地存储分层解法

不同数据有不同存储需求:Token 需要加密、商品缓存需要结构化存储、用户设置只需要简单键值。

DART
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ **安装依赖**: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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(3) 收益:离线可用 + 无感登录

Bob 实现本地存储后,Token 加密持久化实现无感登录,商品缓存实现离线浏览,偏好设置关机不丢失。


3. 存储方案选型

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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
方案 数据类型 加密 容量 适用场景
SharedPreferences KV 简单值 设置、标记
Hive NoSQL 文档 可选 缓存、对象
Drift/SQLite SQL 关系型 订单、历史
SecureStorage KV 加密 Token、密钥

4. SharedPreferences

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:用户偏好设置

DART
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ **安装依赖**: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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:首次启动引导页

DART
import 'package:flutter/material.dart';

// 自定义类定义来源:
// - UserPreferences: 见本课第4节 SharedPreferences 示例

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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

5. Hive NoSQL 数据库

(1) Hive 核心概念

概念 说明
Hive 数据库实例
Box 类似表,存储键值对
TypeAdapter 自定义对象序列化/反序列化
HiveObject 可自动管理 key 的持久化对象

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Hive 商品缓存

DART
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';

// ⚙️ **安装依赖**:flutter pub add hive hive_flutter
// ⚙️ **开发依赖**:flutter pub add --dev hive_generator build_runner

// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型(简化版如下)
/*
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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Hive 购物车持久化

DART
import 'package:hive/hive.dart';

// ⚙️ **安装依赖**:flutter pub add hive hive_flutter

// 自定义类定义来源:
// - CartItem: 见第12课 CartNotifier 示例

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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

6. Drift (SQLite) 关系型数据库

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Drift 订单数据库

DART
import 'package:drift/drift.dart';
import 'package:drift/native.dart';

// ⚙️ **安装依赖**:flutter pub add drift drift_flutter sqlite3_flutter_libs
// ⚙️ **开发依赖**: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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

7. Flutter Secure Storage

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:JWT Token 加密存储

DART
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

// ⚙️ **安装依赖**: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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

8. 完整示例:ShopApp 存储层

DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

// ⚙️ **安装依赖**:flutter pub add flutter_riverpod riverpod_annotation shared_preferences
// ⚙️ **开发依赖**:flutter pub add --dev riverpod_generator build_runner

// 自定义类定义来源:
// - SecureStorage: 见本课第7节 Flutter Secure Storage
// - Product: 见第11课 json_serializable 模型
// - User/AuthService: 见第14课 Auth Notifier
// - productRepositoryProvider: 见第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 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

❓ 常见问题

Q SharedPreferences 存大量数据会怎样?
A SharedPreferences 一次性加载到内存,存大量数据浪费内存。大数据用 Hive 或 Drift。
Q Hive 和 Drift 怎么选?
A Hive 适合缓存和非结构化数据(NoSQL,简单快速);Drift 适合需要复杂查询和关系的结构化数据(SQL,类型安全)。
Q SecureStorage 在 Web 上安全吗?
A Web 端使用 localStorage,没有真正加密。敏感数据在 Web 端应通过后端 Session 管理。
Q Hive 的 typeId 重复了怎么办?
A typeId 必须全局唯一,重复会导致反序列化错误。建议维护 typeId 分配表。
Q 数据库迁移(schema version 升级)怎么做?
A Drift 用 onUpgrade 回调处理迁移。Hive 用 box.deleteAndSaveFromStorage() 或手动数据转换。
Q Hive 和 Isar 有什么关系?
A Isar 是 Hive 作者的下一代产品,性能更好但 API 不同。Hive 仍广泛使用且稳定。

📖 小节


📝 作业

  1. 基础题(难度⭐):用 SharedPreferences 实现暗色模式持久化,重启应用后保持上次选择。
  2. 进阶题(难度⭐⭐):用 Hive 实现购物车本地持久化,关闭应用后重新打开购物车数据仍在。
  3. 挑战题(难度⭐⭐⭐):实现完整的离线缓存策略:优先读 Hive 缓存显示,后台请求 API 更新数据,更新后刷新 UI 和缓存。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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