sqflite + Model Class
Hubungkan database dengan model Dart yang type-safe.
Kenapa SQL Mentah Tidak Boleh Tinggal di Widget
Setiap kali widget memanggil db.query('produk') secara langsung, ada tiga masalah yang tumbuh diam-diam. Pertama, SQL yang sama ditulis ulang di banyak tempat, dan satu perubahan nama kolom memaksa kamu berburu ke seluruh codebase. Kedua, widget jadi mustahil diuji tanpa database sungguhan, padahal logika bisnisnya sederhana. Ketiga, kalau suatu hari datanya pindah ke REST API, semua widget harus ditulis ulang. Repository pattern menyelesaikan ketiganya dengan satu pintu: widget hanya bicara ke class Dart biasa, dan class itulah yang tahu soal SQL. Widget tidak pernah melihat satu baris pun query.
Model Class: Kontrak Type-Safe
Model adalah janji bentuk data. Tanpa model, kamu melempar Map<String, Object?> ke mana-mana dan setiap akses kolom rawan salah ketik yang baru ketahuan saat aplikasi jalan. Dengan model, salah ketik jadi compile error yang ketahuan sebelum aplikasi dibuka:
class Produk {
final int? id;
final String nama;
final int harga;
const Produk({this.id, required this.nama, required this.harga});
Map<String, Object?> toMap() =>
{'id': id, 'nama': nama, 'harga': harga};
factory Produk.fromMap(Map<String, Object?> map) => Produk(
id: map['id'] as int?,
nama: map['nama'] as String,
harga: (map['harga'] as num).toInt(),
);
Produk copyWith({int? id, String? nama, int? harga}) => Produk(
id: id ?? this.id,
nama: nama ?? this.nama,
harga: harga ?? this.harga,
);
}Perhatikan detail null-safety: id bertipe int? karena sebelum baris disimpan, id belum ada (autoincrement diisi SQLite saat insert). copyWith membuat model immutable tapi tetap mudah diubah, pola yang akrab kalau kamu memakai Riverpod atau BLoC. Simpan tanggal sebagai ISO string (DateTime.now().toIso8601String()) agar gampang di-sort dan dibaca manusia, bukan timestamp mentah.
Repository: CRUD Lengkap dalam Satu Pintu
Repository membungkus semua operasi database. Versi lengkapnya bukan cuma baca dan tambah, tapi juga ubah, hapus, dan operasi massal yang aman:
class ProdukRepo {
Future<Database> get _db async => bukaDb();
Future<int> tambah(Produk p) async {
final db = await _db;
return db.insert('produk', p.toMap());
}
Future<List<Produk>> semua() async {
final db = await _db;
final rows = await db.query('produk', orderBy: 'nama ASC');
return rows.map(Produk.fromMap).toList();
}
Future<void> ubah(Produk p) async {
final db = await _db;
await db.update('produk', p.toMap(),
where: 'id = ?', whereArgs: [p.id]);
}
Future<void> hapus(int id) async {
final db = await _db;
await db.delete('produk', where: 'id = ?', whereArgs: [id]);
}
Future<void> tambahBanyak(List<Produk> list) async {
final db = await _db;
await db.transaction((txn) async {
for (final p in list) {
await txn.insert('produk', p.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace);
}
});
}
}whereArgs wajib dipakai, jangan pernah menggabungkan string ke query karena itu pintu SQL injection. transaction menjamin operasi massal bersifat atomik: kalau satu insert gagal, semuanya dibatalkan, bukan setengah jalan.
Catatan teknis: Pola repository memisahkan akses data dari UI. Nanti gampang diganti ke API/REST tanpa mengubah widget, cukup tulis implementasi repo baru dengan method yang sama.
Edge Cases yang Sering Menjebak
Migrasi skema adalah jebakan klasik: menambah kolom tanpa onUpgrade membuat aplikasi crash di device pengguna lama. Selalu naikkan versi database dan tulis migrasi (ALTER TABLE ... ADD COLUMN). Untuk kolom yang sering dicari (misalnya nama produk), tambahkan CREATE INDEX agar query LIKE tidak melambat saat data ribuan. Dan uji repository dengan database in-memory (openDatabase(inMemoryDatabasePath)) supaya unit test jalan cepat tanpa emulator.
Kapan Pendekatan Ini Berlebihan
Untuk prototipe sekali pakai yang cuma butuh dua query, repository penuh memang overkill, query langsung boleh saja. Untuk data sangat relasional dengan join kompleks atau butuh sinkronisasi cloud dua arah, pertimbangkan Drift atau Isar yang punya type-safe query builder bawaan.
Tantangan
Repo catatan
Buat class Catatan (id?, isi, tanggal String) dengan toMap/fromMap, plus CatatanRepo dengan method semua() dan tambah().