Best Practice #
Memilih library local storage yang tepat hanyalah langkah pertama. Bagaimana kamu menggunakannya — di mana menyimpan data, kapan mengaksesnya, bagaimana mengujinya — sama pentingnya. Artikel ini merangkum pola-pola yang berlaku lintas library dan menentukan kualitas implementasi local storage di aplikasi produksi.
1. Selalu Abstraksi di Balik Interface #
Jangan akses library storage langsung dari widget atau notifier — buat layer abstraksi yang bisa diganti dan ditest:
// ANTI-PATTERN: akses Hive langsung dari Notifier
class ProdukNotifier extends AsyncNotifier<List<Produk>> {
@override
Future<List<Produk>> build() async {
final box = Hive.box<Produk>('produk'); // bergantung langsung pada Hive!
return box.values.toList();
}
}
// BENAR: abstraksi di balik interface
abstract class ProdukLocalDataSource {
Future<List<Produk>> getAll();
Future<void> saveAll(List<Produk> produk);
Future<void> clear();
}
class HiveProdukDataSource implements ProdukLocalDataSource {
final Box<Produk> _box;
HiveProdukDataSource(this._box);
@override
Future<List<Produk>> getAll() async => _box.values.toList();
@override
Future<void> saveAll(List<Produk> produk) async {
await _box.clear();
await _box.putAll({for (final p in produk) p.id: p});
}
@override
Future<void> clear() async => _box.clear();
}
// Mock untuk test -- tanpa sentuh Hive sama sekali
class MockProdukDataSource implements ProdukLocalDataSource {
final List<Produk> _data;
MockProdukDataSource([this._data = const []]);
@override
Future<List<Produk>> getAll() async => _data;
@override
Future<void> saveAll(List<Produk> produk) async {}
@override
Future<void> clear() async {}
}
2. Jangan Simpan Data Sensitif Sembarangan #
// JANGAN: simpan data sensitif di storage yang tidak terenkripsi
await SharedPreferences.getInstance().then((p) {
p.setString('access_token', token); // SharedPrefs tidak terenkripsi!
p.setString('pin', userPin); // sangat berbahaya!
p.setString('kartu_kredit', cardNumber); // fatal!
});
// JANGAN: simpan data sensitif di Hive tanpa enkripsi
final box = Hive.box('sensitive');
await box.put('pin', userPin); // tidak aman jika tanpa HiveAesCipher
// BENAR: gunakan flutter_secure_storage untuk data sensitif
const storage = FlutterSecureStorage();
await storage.write(key: 'access_token', value: token);
await storage.write(key: 'refresh_token', value: refreshToken);
// BENAR: Hive dengan enkripsi untuk data sensitif yang kompleks
final encryptedBox = await openEncryptedBox('sensitive_data');
await encryptedBox.put('pin', hashedPin);
// Klasifikasi data:
// flutter_secure_storage: token auth, PIN, password, kunci enkripsi
// Hive terenkripsi: data sensitif yang perlu query/filter
// SharedPrefs (TIDAK OK): JANGAN untuk data sensitif apapun
// ObjectBox/Drift: data bisnis normal (bukan rahasia)
3. Strategi Cache yang Tepat #
// ANTI-PATTERN: tidak ada TTL -- data cache tidak pernah expired
class ProdukCache {
final Box<Produk> _box;
ProdukCache(this._box);
Future<List<Produk>?> get() async {
return _box.isEmpty ? null : _box.values.toList();
// Akan terus mengembalikan data lama selamanya!
}
}
// BENAR: cache dengan TTL dan invalidasi yang jelas
class ProdukCache {
static const _ttl = Duration(minutes: 30);
final Box<Produk> _box;
final Box _metaBox;
Future<List<Produk>?> get() async {
final cachedAt = _metaBox.get('produk_cached_at') as int?;
if (cachedAt == null) return null;
final elapsed = DateTime.now().millisecondsSinceEpoch - cachedAt;
if (elapsed > _ttl.inMilliseconds) {
await invalidate();
return null;
}
return _box.values.toList();
}
Future<void> save(List<Produk> produk) async {
await _box.clear();
await _box.putAll({for (final p in produk) p.id: p});
await _metaBox.put('produk_cached_at', DateTime.now().millisecondsSinceEpoch);
}
Future<void> invalidate() async {
await _box.clear();
await _metaBox.delete('produk_cached_at');
}
}
// Kapan invalidate cache?
// ✓ Setelah operasi write (tambah, update, hapus)
// ✓ Saat pengguna melakukan pull-to-refresh
// ✓ Saat TTL habis
// ✓ Saat logout pengguna
4. Inisialisasi Awal yang Benar #
// ANTI-PATTERN: inisialisasi di mana saja, bisa race condition
class SomeWidget extends StatefulWidget {
@override
State<SomeWidget> createState() => _SomeWidgetState();
}
class _SomeWidgetState extends State<SomeWidget> {
@override
void initState() {
super.initState();
Hive.initFlutter(); // async tapi tidak di-await -- berbahaya!
}
}
// BENAR: semua inisialisasi selesai sebelum runApp()
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // wajib sebelum async call
// Inisialisasi berurutan
await Hive.initFlutter();
Hive.registerAdapter(ProdukAdapter());
await Hive.openBox<Produk>('produk');
await Hive.openBox('settings');
final prefs = await SharedPreferences.getInstance();
final obxStore = await ObjectBoxStore.create();
final db = AppDatabase();
runApp(
ProviderScope(
overrides: [
prefsProvider.overrideWithValue(AppPreferences(prefs)),
objectBoxProvider.overrideWithValue(obxStore),
databaseProvider.overrideWithValue(db),
],
child: const MyApp(),
),
);
}
5. Bersihkan Data Saat Logout #
class LogoutService {
final AppPreferences _prefs;
final Box _hiveCache;
final ObjectBoxStore _obx;
final AppDatabase _db;
Future<void> logout() async {
// 1. Hapus token dari secure storage
const storage = FlutterSecureStorage();
await storage.deleteAll();
// 2. Bersihkan preferensi pengguna (tapi tidak semua!)
await _prefs.clearUserData(); // hapus hanya data yang user-specific
// Jangan hapus: isDarkMode, bahasa -- itu preferensi device, bukan user
// 3. Bersihkan cache data
await _hiveCache.clear();
// 4. Hapus data user dari ObjectBox/Drift jika ada
_obx.produkBox.removeAll();
await (_db.delete(_db.produks)).go();
// 5. Arahkan ke halaman login
}
}
6. Migrasi yang Aman #
// Drift -- migrasi schema yang aman
@override
MigrationStrategy get migration {
return MigrationStrategy(
onUpgrade: (m, from, to) async {
// SELALU gunakan transaksi untuk migrasi
await transaction(() async {
if (from < 2) {
// AMAN: addColumn dengan default value
await m.addColumn(produks, produks.stok);
// AMAN: buat tabel baru
await m.createTable(pesananProduks);
}
if (from < 3) {
// HATI-HATI: rename kolom (buat baru, copy, hapus lama)
await customStatement(
'ALTER TABLE produks RENAME COLUMN harga_lama TO harga',
);
}
if (from < 4) {
// AMAN: tambah index untuk performa query
await customStatement(
'CREATE INDEX idx_produks_kategori ON produks(kategori_id)',
);
}
});
},
);
}
// JANGAN lakukan ini saat migrasi:
// ✗ Hapus kolom yang masih berisi data penting tanpa backup
// ✗ Ubah tipe kolom yang tidak kompatibel
// ✗ Rename tabel tanpa update semua referensi
7. Hindari Query di Build Method #
// ANTI-PATTERN: query database di build() -- dipanggil setiap rebuild!
class ProdukScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final produk = database.produkDao.getAll(); // Future dipanggil ulang!
return FutureBuilder(future: produk, builder: ...);
}
}
// BENAR: query di Notifier, build() hanya render
class ProdukNotifier extends AsyncNotifier<List<Produk>> {
@override
Future<List<Produk>> build() {
return ref.watch(produkDaoProvider).getAll(); // sekali saja
}
}
class ProdukScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final produkAsync = ref.watch(produkProvider);
return produkAsync.when(
data: (produk) => ProdukList(produk: produk),
loading: () => const CircularProgressIndicator(),
error: (e, _) => ErrorView(pesan: e.toString()),
);
}
}
8. Testing Storage Layer #
// Test Drift dengan in-memory database
AppDatabase createTestDatabase() {
return AppDatabase(NativeDatabase.memory());
}
void main() {
group('ProdukDao', () {
late AppDatabase db;
setUp(() => db = createTestDatabase());
tearDown(() => db.close());
test('insert dan get produk', () async {
final id = await db.produkDao.insert(
ProduksCompanion.insert(
nama: 'Test Produk',
harga: 10000,
kategoriId: 1,
),
);
final produk = await db.produkDao.getById(id);
expect(produk?.nama, 'Test Produk');
});
test('watchAll emit data saat ada insert', () async {
final stream = db.produkDao.watchAll();
// Expect: pertama emit [], lalu emit [produk] setelah insert
expectLater(
stream,
emitsInOrder([
isEmpty,
hasLength(1),
]),
);
await db.produkDao.insert(
ProduksCompanion.insert(nama: 'Produk A', harga: 5000, kategoriId: 1),
);
});
});
// Test Hive dengan path sementara
group('ProdukCache', () {
late Box<Produk> box;
late Directory tempDir;
setUp(() async {
tempDir = await Directory.systemTemp.createTemp('hive_test');
Hive.init(tempDir.path);
Hive.registerAdapter(ProdukAdapter());
box = await Hive.openBox<Produk>('test_produk');
});
tearDown(() async {
await box.close();
await Hive.deleteBoxFromDisk('test_produk');
await tempDir.delete(recursive: true);
});
test('save dan load produk', () async {
final cache = ProdukCache(box: box);
final produk = [Produk(id: '1', nama: 'A', harga: 1000)];
await cache.save(produk);
final loaded = await cache.get();
expect(loaded?.length, 1);
expect(loaded?.first.nama, 'A');
});
});
}
Anti-Pattern yang Harus Dihindari #
// ✗ Buka box berulang kali -- mahal!
Future<void> simpan(Produk p) async {
final box = await Hive.openBox<Produk>('produk'); // buka setiap kali!
await box.put(p.id, p);
}
// ✓ Buka sekali di startup, akses via Hive.box<T>('nama')
// ✗ Simpan List besar di SharedPreferences
await prefs.setString('semua_produk', jsonEncode(daftarPanjang));
// ✓ Gunakan Hive atau Drift untuk list yang panjang
// ✗ Tidak close query ObjectBox
final query = box.query().build();
final data = query.find();
// query.close(); // LUPA! memory leak
// ✓ Selalu close query setelah selesai, atau gunakan try-finally
// ✗ Tulis ke storage di setiap keystroke
TextField(
onChanged: (v) async {
await prefs.setString('draft', v); // tulis setiap karakter!
},
)
// ✓ Debounce atau simpan saat onEditingComplete / onSubmitted
// ✗ Akses storage di main isolate tanpa async
final data = Hive.box('data').get('key'); // OK karena Hive sync
final result = database.select(database.produks).get(); // ERROR: missing await!
// ✓ Selalu await operasi async
Checklist Review Local Storage #
SETUP:
□ Semua storage diinisialisasi di main() sebelum runApp()
□ Satu instance database/store per app (tidak dibuat ulang)
□ Storage layer diakses via interface / abstraksi, bukan langsung
KEAMANAN:
□ Token dan data sensitif menggunakan flutter_secure_storage
□ Data yang di-enkripsi menggunakan key yang disimpan di secure storage
□ Data user dihapus saat logout
CACHING:
□ Cache punya TTL yang jelas
□ Cache diinvalidasi setelah operasi write
□ Ada fallback jika cache kosong atau expired
PERFORMA:
□ Box Hive dibuka sekali saat startup, diakses tanpa await
□ Query ObjectBox selalu di-close setelah selesai
□ Tidak ada query di build() method
□ Operasi batch menggunakan putMany/putAll bukan loop put()
TESTING:
□ Storage layer di-abstraksi di balik interface
□ Ada mock implementation untuk unit test
□ Test Drift menggunakan NativeDatabase.memory()
□ Test Hive menggunakan direktori sementara
MIGRASI (jika pakai Drift):
□ schemaVersion di-increment setiap ada perubahan schema
□ onUpgrade menangani semua versi lama
□ Migrasi ditest sebelum rilis
Ringkasan #
- Selalu abstraksi storage di balik interface — jangan akses Hive/Drift/ObjectBox langsung dari widget atau notifier. Ini membuat code lebih mudah ditest dan diganti.
- Jangan simpan data sensitif di SharedPreferences atau Hive tanpa enkripsi. Gunakan
flutter_secure_storageuntuk token, PIN, dan credential.- Cache butuh TTL (Time To Live) yang jelas dan mekanisme invalidasi setelah operasi write — cache yang tidak pernah expired adalah bug yang tertunda.
- Inisialisasi semua storage sebelum
runApp()— bukan di initState() widget, bukan di lazy initialization yang tidak ter-await.- Bersihkan data pengguna saat logout — token di secure storage, cache di Hive, data user di ObjectBox/Drift.
- Jangan query di
build()method — lakukan di Notifier/Repository, biarkan widget hanya merender hasilnya.- Test storage layer menggunakan in-memory database (Drift) atau direktori sementara (Hive) — jangan sentuh storage device yang nyata saat testing.
- Gunakan operasi batch (
putMany,putAll) alih-alih loopput()tunggal — jauh lebih efisien untuk data dalam jumlah besar.