Flutter: ローカルストレージ
永続化のないデータは朝露のようなもの — アプリを閉じると消えてしまいます。ローカルストレージはデータにデバイス上の居場所を与えます。
📋 前提条件: 以下を先にマスターしている必要があります
- レッスン12:状態管理 — Riverpod
1. このレッスンで学ぶこと
- SharedPreferences:キー・バリューストレージ(ユーザー設定、テーマ設定、初回起動フラグ)
- Hive:NoSQL軽量データベース、TypeAdapterによるカスタムオブジェクトシリアライズ
- sqflite / Drift:リレーショナルデータベース、DAOパターンとマイグレーション戦略
- flutter_secure_storage:暗号化ストレージ(トークン、鍵)
- ShopApp:Hive商品キャッシュ + SecureStorageによるJWT + SharedPreferencesによるユーザー設定
2. オフラインでデータを失うストーリー
(1) 悩み:起動のたびにデータがリセットされる
BobのShopAppユーザーが報告する問題:アプリを開くたびにカートが空で、お気に入り商品が消え、テーマ設定がデフォルトに戻る。これはすべてのデータがメモリにしか存在しないため — アプリを閉じるとすべて失われます。さらに悪いことに、JWTトークンもメモリに保存されており、ページ切り替え時に紛失し、ユーザーに繰り返しログインを強制します。
(2) 階層型ストレージソリューション
データによってストレージのニーズは異なります:トークンには暗号化が必要、商品キャッシュには構造化ストレージが必要、ユーザー設定にはシンプルなキー・バリューペアだけで十分です。
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
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(3) メリット:オフライン利用 + シームレスなログイン
ローカルストレージを実装すると、Bobは暗号化されたトークンの永続化によるシームレスなログイン、オフライン閲覧のための商品キャッシュ、アプリ再起動後も保持される設定を手に入れます。
3. ストレージソリューションの選択
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暗号化 | ✅ | 小 | トークン、鍵 |
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';
// ⚙️ 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
> 出力: ローカルの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';
// 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
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
5. Hive NoSQLデータベース
(1) Hiveのコア概念
| 概念 | 説明 |
|---|---|
| Hive | データベースインスタンス |
| Box | テーブルに似ており、キー・バリューペアを格納 |
| TypeAdapter | カスタムオブジェクトのシリアライズ/デシリアライズ |
| HiveObject | 自動管理キーを持つ永続オブジェクト |
▶ サンプル
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';
// ⚙️ 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
> 出力: ローカルの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';
// ⚙️ 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
> 出力: ローカルの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';
// ⚙️ 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
> 出力: ローカルの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トークン暗号化ストレージ
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
> 出力: ローカルの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';
// ⚙️ 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
> 出力: ローカルの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 Web上でSecureStorageは安全ですか?
A WebではlocalStorageが使用され、本当の暗号化はありません。Web上の機密データはバックエンドのセッションで管理してください。
Q HiveのtypeIdが重複したらどうなりますか?
A typeIdはグローバルに一意である必要があります — 重複するとデシリアライズエラーが発生します。typeId割り当て表を管理してください。
Q データベースマイグレーション(スキーマバージョンアップ)はどう処理しますか?
A Driftは
onUpgradeコールバックでマイグレーションを行います。Hiveはbox.deleteAndSaveFromStorage()または手動のデータ変換を使用します。Q HiveとIsarの関係は何ですか?
A IsarはHive作者による次世代製品で、パフォーマンスは優れていますがAPIが異なります。Hiveは依然として広く使用され安定しています。
📖 まとめ
- SharedPreferencesはシンプルなキー・バリューペア(設定/フラグ)を保存し、大量データには不適切
- Hive NoSQLはオブジェクトキャッシュに適し、TypeAdapterがカスタムシリアライズをサポート
- Drift(SQLite)はリレーショナルデータ(注文/履歴)に適し、型安全 + マイグレーションサポート付き
- SecureStorageは機密情報(トークン/鍵)を暗号化し、AndroidはEncryptedSharedPreferencesを使用
- オフラインファースト戦略:キャッシュ優先 + ネットワークフォールバック、Riverpod統合でシームレスな切り替え
📝 練習問題
- 基本 (⭐):SharedPreferencesを使ってダークモード選択を永続化し、アプリ再起動後も選択が保持されるようにしてください。
- 中級 (⭐⭐):Hiveを使ってショッピングカートをローカルに永続化してください — アプリを閉じて再び開いてもカートデータが保持されるようにします。
- チャレンジ (⭐⭐⭐):完全なオフラインキャッシュ戦略を実装してください:Hiveキャッシュを先に読み込んで表示し、バックグラウンドでAPIから取得し、更新後にUIとキャッシュをリフレッシュします。