Chương 11 · Local Storage

Local Storage

Mọi app cần lưu state local: setting, cache API, draft form, offline data. Flutter có 5+ thư viện — chọn sai → migration đau. Chương này dạy 5 thư viện chính + decision framework.

Độ dài: ~1090 dòng Bài tập: 5 Quiz: 8 Prerequisites: Ch 10 + Dart async
🎯 Mục tiêu chương
  • Phân biệt key-value vs SQL vs NoSQL cho local.
  • SharedPreferences cho key-value primitive.
  • flutter_secure_storage cho sensitive data — encrypted.
  • sqflite cho SQL relational.
  • drift (codegen từ sqflite) cho type-safe SQL.
  • hive cho NoSQL key-value object.
  • isar cho NoSQL object DB modern.
  • Pattern offline-first.
  • Migration: schema thay đổi giữa version app.

1. Spectrum lưu trữ local

OptionTypeUse caseEncrypt?Speed
In-memory (state)VolatileSession state, không persistNhanh nhất
SharedPreferencesKey-value primitiveSetting, flag (theme, locale, intro shown)KhôngNhanh
SecureStorageKey-value encryptedToken, passwordCó (Keychain/Keystore)Chậm hơn
File systemFileImage cache, JSON dumpKhôngTrung bình
sqflite (SQLite)SQL relationalNote, transaction, complex queryKhông (mặc định)Trung bình
driftSQL + codegenSame as sqflite, type-safeTương tựTrung bình
HiveNoSQL objectObject cache, simple modelOptionalRất nhanh
IsarNoSQL object + queryModern offline-first, full DBOptionalNhanh

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).
⚠️ SecureStorage không hoàn hảo

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

Offline-first flow App start Read local (Hive/SQLite) Show UI Fetch API (background) Update local + UI refresh
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

🎯 Chọn storage nào?
  • 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

Q1

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.

Q2

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.

Q3

sqflite migration — callback nào?

Xem đáp án

Đáp án: onCreate (lần đầu install) và onUpgrade(db, oldV, newV) (khi version tăng).

Q4

Drift cần codegen — lệnh?

Xem đáp án

Đáp án: dart run build_runner build. Hoặc watch mode để auto.

Q5

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.

Q6

Cache TTL pattern — implement?

Xem đáp án

Đáp án: Lưu timestamp khi cache. Đọc: check now - timestamp > ttl → invalidate fetch lại.

Q7

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.

Q8

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.