- Phân biệt key-value vs SQL vs NoSQL cho local.
SharedPreferencescho key-value primitive.flutter_secure_storagecho sensitive data — encrypted.sqflitecho SQL relational.drift(codegen từ sqflite) cho type-safe SQL.hivecho NoSQL key-value object.isarcho NoSQL object DB modern.- Pattern offline-first.
- Migration: schema thay đổi giữa version app.
1. Spectrum lưu trữ local
| Option | Type | Use case | Encrypt? | Speed |
|---|---|---|---|---|
| In-memory (state) | Volatile | Session state, không persist | — | Nhanh nhất |
| SharedPreferences | Key-value primitive | Setting, flag (theme, locale, intro shown) | Không | Nhanh |
| SecureStorage | Key-value encrypted | Token, password | Có (Keychain/Keystore) | Chậm hơn |
| File system | File | Image cache, JSON dump | Không | Trung bình |
| sqflite (SQLite) | SQL relational | Note, transaction, complex query | Không (mặc định) | Trung bình |
| drift | SQL + codegen | Same as sqflite, type-safe | Tương tự | Trung bình |
| Hive | NoSQL object | Object cache, simple model | Optional | Rất nhanh |
| Isar | NoSQL object + query | Modern offline-first, full DB | Optional | Nhanh |
2. shared_preferences
flutter pub add shared_preferences
import 'package:shared_preferences/shared_preferences.dart';
final prefs = await SharedPreferences.getInstance();
// Write
await prefs.setString('name', 'Việt');
await prefs.setInt('age', 25);
await prefs.setBool('premium', true);
await prefs.setDouble('balance', 99.5);
await prefs.setStringList('tags', ['flutter', 'dart']);
// Read — sync (đã getInstance trước đó)
final name = prefs.getString('name'); // nullable
final age = prefs.getInt('age') ?? 0;
// Delete
await prefs.remove('name');
await prefs.clear(); // xóa tất cả
Wrapper class — typed access
class AppPrefs {
final SharedPreferences _prefs;
AppPrefs(this._prefs);
static Future<AppPrefs> create() async {
return AppPrefs(await SharedPreferences.getInstance());
}
ThemeMode get themeMode {
final i = _prefs.getInt('themeMode') ?? 0;
return ThemeMode.values[i];
}
Future<void> setThemeMode(ThemeMode m) => _prefs.setInt('themeMode', m.index);
bool get hasSeenIntro => _prefs.getBool('hasSeenIntro') ?? false;
Future<void> markIntroSeen() => _prefs.setBool('hasSeenIntro', true);
}
Type-safe + tránh string key rải khắp code.
3. flutter_secure_storage
flutter pub add flutter_secure_storage
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
const storage = FlutterSecureStorage();
// Write
await storage.write(key: 'jwt', value: 'eyJhbGc...');
// Read
final token = await storage.read(key: 'jwt');
// Delete
await storage.delete(key: 'jwt');
await storage.deleteAll();
Storage layer per platform:
- iOS — Keychain (system encryption).
- Android — EncryptedSharedPreferences (Keystore-backed).
- macOS — Keychain.
- Linux — libsecret.
- Windows — Windows Credential Manager.
- Web — không support (browser không có Keychain — dùng IndexedDB encrypted).
Encrypted ngừa file system attack (đọc disk dump). Nhưng app vẫn decrypt được → root device / debugger có thể đọc. Cho sensitive data extreme (medical record, finance): xem xét backend session ngắn + biometric reauth.
4. File system — path_provider + dart:io
flutter pub add path_provider
import 'package:path_provider/path_provider.dart';
import 'dart:io';
// Documents directory — backup khi user backup device, persistent
final docs = await getApplicationDocumentsDirectory();
// Cache directory — OS có thể clear khi cần memory
final cache = await getTemporaryDirectory();
// Write file
final file = File('${docs.path}/data.json');
await file.writeAsString(jsonEncode({'key': 'value'}));
// Read
if (await file.exists()) {
final content = await file.readAsString();
print(jsonDecode(content));
}
5. sqflite — SQLite
flutter pub add sqflite path
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
Future<Database> openDb() async {
final dbPath = join(await getDatabasesPath(), 'notes.db');
return openDatabase(
dbPath,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT,
created_at INTEGER NOT NULL
)
''');
},
);
}
CRUD
// Insert
final id = await db.insert('notes', {
'title': 'Học Flutter',
'body': 'IT Basic chương 11',
'created_at': DateTime.now().millisecondsSinceEpoch,
});
// Query — all
final all = await db.query('notes');
// List<Map<String, dynamic>>
// Query — where
final rows = await db.query(
'notes',
where: 'id = ?',
whereArgs: [1],
);
// Raw SQL
final r = await db.rawQuery(
'SELECT * FROM notes WHERE created_at > ? ORDER BY id DESC LIMIT 10',
[DateTime.now().subtract(Duration(days: 7)).millisecondsSinceEpoch],
);
// Update
await db.update('notes', {'title': 'Updated'}, where: 'id = ?', whereArgs: [1]);
// Delete
await db.delete('notes', where: 'id = ?', whereArgs: [1]);
Migration với onUpgrade
openDatabase(
dbPath,
version: 2, // tăng từ 1 → 2
onCreate: (db, v) async {
// Schema mới nhất
},
onUpgrade: (db, oldV, newV) async {
if (oldV < 2) {
await db.execute('ALTER TABLE notes ADD COLUMN priority INTEGER DEFAULT 0');
}
// Future migrations: if (oldV < 3) ...
},
)
6. drift — SQL type-safe codegen
flutter pub add drift drift_flutter
flutter pub add --dev drift_dev build_runner
import 'package:drift/drift.dart';
part 'database.g.dart';
class Notes extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 100)();
TextColumn get body => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
}
@DriftDatabase(tables: [Notes])
class AppDb extends _$AppDb {
AppDb() : super(_openConnection());
@override
int get schemaVersion => 1;
// Type-safe query
Future<List<Note>> recentNotes() =>
(select(notes)..orderBy([(t) => OrderingTerm.desc(t.createdAt)])).get();
// Watch — Stream cho real-time UI
Stream<List<Note>> watchAll() => select(notes).watch();
Future<int> addNote(NotesCompanion n) => into(notes).insert(n);
}
// Use
final db = AppDb();
// StreamBuilder cho real-time
StreamBuilder<List<Note>>(
stream: db.watchAll(),
builder: (ctx, snap) => ListView(
children: (snap.data ?? []).map((n) => ListTile(title: Text(n.title))).toList(),
),
)
7. hive — NoSQL object
flutter pub add hive hive_flutter
flutter pub add --dev hive_generator build_runner
import 'package:hive/hive.dart';
part 'user.g.dart';
@HiveType(typeId: 0)
class User extends HiveObject {
@HiveField(0)
String name;
@HiveField(1)
int age;
User({required this.name, required this.age});
}
// Init
await Hive.initFlutter();
Hive.registerAdapter(UserAdapter());
// Open box
final box = await Hive.openBox<User>('users');
// Put / get
await box.put('viet', User(name: 'Việt', age: 25));
final u = box.get('viet');
// Iterate
for (final key in box.keys) {
print('$key: ${box.get(key)?.name}');
}
// Delete
await box.delete('viet');
await box.clear();
8. isar — modern NoSQL
Cùng author Hive nhưng next-gen: full-text search, indexes, query DSL mạnh hơn.
flutter pub add isar isar_flutter_libs
flutter pub add --dev isar_generator build_runner
import 'package:isar/isar.dart';
part 'user.g.dart';
@collection
class User {
Id id = Isar.autoIncrement;
@Index(type: IndexType.value, caseSensitive: false)
late String name;
late int age;
}
// Open
final dir = await getApplicationDocumentsDirectory();
final isar = await Isar.open([UserSchema], directory: dir.path);
// Write
await isar.writeTxn(() async {
await isar.users.put(User()
..name = 'Việt'
..age = 25);
});
// Query
final youngUsers = await isar.users
.filter()
.ageLessThan(30)
.sortByName()
.findAll();
// Watch
isar.users.where().watch(fireImmediately: true).listen((users) {
print('${users.length} users');
});
9. Offline-first pattern
class UserRepository {
final Dio _api;
final Box<User> _cache;
UserRepository(this._api, this._cache);
Stream<List<User>> watchUsers() async* {
// 1. Emit cached ngay
yield _cache.values.toList();
// 2. Fetch fresh từ API
try {
final r = await _api.get('/users');
final fresh = (r.data as List).map((j) => User.fromJson(j)).toList();
// 3. Update cache
await _cache.clear();
for (final u in fresh) {
await _cache.put(u.id, u);
}
// 4. Emit fresh
yield fresh;
} catch (_) {
// Network fail — vẫn có cache đã yield ở step 1
}
}
}
10. Cache TTL pattern
class CachedData<T> {
final T data;
final DateTime savedAt;
final Duration ttl;
bool get isExpired => DateTime.now().difference(savedAt) > ttl;
const CachedData({required this.data, required this.savedAt, required this.ttl});
}
// Use
final cached = box.get('users');
if (cached != null && !cached.isExpired) {
return cached.data;
}
// else: fetch fresh
11. Decision framework
- Setting simple (theme, locale, flag): SharedPreferences.
- Token, password: SecureStorage.
- Cache API JSON list: Hive hoặc file.
- Relational data (user + post + comment join): Drift.
- Offline-first full app: Drift hoặc Isar.
12. Bài tập
Setting page persistent
Setting page: dark mode toggle, locale dropdown, font size slider. Persist qua SharedPreferences. App restart giữ setting.
💡 Gợi ý đáp án
Wrapper class AppPrefs (section 2). Init ở main() trước runApp. Provide qua Provider hoặc Riverpod.
JWT secure + auto login
Login form lưu token vào SecureStorage. App start check token, auto-login nếu valid.
💡 Gợi ý đáp án
Splash screen check SecureStorage token. Có → navigate home. Không → login. Token expired → refresh hoặc logout.
Note app sqflite
Note app CRUD với sqflite. Schema: id, title, body, createdAt. List, add, edit, delete.
💡 Gợi ý đáp án
Tham khảo section 5. DAO class wrap CRUD. UI list + form modal cho add/edit.
Migrate sqflite → drift
Migrate bài 3 từ sqflite raw sang drift. So sánh code (type-safety, watch Stream).
💡 Gợi ý đáp án
Lợi: .watch() trả Stream tự động cập nhật UI khi DB đổi. Type-safe column access (compile-time check).
Offline-first list user
Fetch list user từ API → cache Hive → next open hiển thị cache trước, fetch background, update.
💡 Gợi ý đáp án
Tham khảo section 9 (offline-first pattern). Bloc emit Loading (initial) → Loaded(cache) → Loaded(fresh).
13. Quiz
SharedPreferences lưu được type gì?
Xem đáp án
Đáp án: primitive: bool, int, double, String, List<String>. Map/custom object phải encode JSON.
SecureStorage tốc độ vs SharedPreferences?
Xem đáp án
Đáp án: Chậm hơn (encrypt overhead). Dùng cho sensitive only — token, password. Setting thường vẫn dùng SharedPrefs.
sqflite migration — callback nào?
Xem đáp án
Đáp án: onCreate (lần đầu install) và onUpgrade(db, oldV, newV) (khi version tăng).
Drift cần codegen — lệnh?
Xem đáp án
Đáp án: dart run build_runner build. Hoặc watch mode để auto.
Hive vs Isar — khác chính?
Xem đáp án
Đáp án: Isar modern hơn (cùng author), có index + query mạnh hơn. Recommend Isar cho new project. Hive vẫn dùng được, ecosystem nhiều hơn.
Cache TTL pattern — implement?
Xem đáp án
Đáp án: Lưu timestamp khi cache. Đọc: check now - timestamp > ttl → invalidate fetch lại.
SharedPreferences sync API có không?
Xem đáp án
Đáp án: Mọi method async. Mới có SharedPreferencesAsync + SharedPreferencesWithCache (2024+) cho sync sau lần init.
SQLite vs NoSQL — chọn nào?
Xem đáp án
Đáp án: SQL: relational + complex query (JOIN). NoSQL: object-oriented, ít join, fast read. Phụ thuộc data shape.
14. Tổng kết
- ✅ Spectrum 6+ option storage local.
- ✅ SharedPreferences — primitive setting.
- ✅ SecureStorage — token, password (encrypted per platform).
- ✅ sqflite — SQL relational, migration qua onUpgrade.
- ✅ Drift — sqflite + codegen type-safe + Stream watch.
- ✅ Hive / Isar — NoSQL object DB. Isar modern hơn.
- ✅ Offline-first: cache trước, fetch sau, update UI khi có fresh.
- ✅ Cache TTL pattern.
- ✅ Decision framework theo data shape + use case.
15. Kết nối
- Ch 10: cache API response.
- Ch 6, 7: state mgmt quản lý loading từ cache.
- Ch 14: test repo với mock storage.