Drift #
Drift adalah library persistence reaktif untuk Flutter dan Dart yang dibangun di atas SQLite. Ia menggabungkan kekuatan SQL penuh dengan API Dart yang type-safe — query diperiksa saat compile time, bukan runtime. Jika aplikasimu butuh relasi antar tabel yang kompleks, JOIN, aggregasi, atau migrasi schema yang terkelola dengan baik, Drift adalah pilihan terbaik.
Instalasi #
# pubspec.yaml
dependencies:
drift: ^2.23.1
drift_flutter: ^0.2.4 # helper untuk Flutter (path, setup)
dev_dependencies:
drift_dev: ^2.23.1
build_runner: ^2.4.13
Definisi Tabel #
Tabel didefinisikan sebagai class Dart yang meng-extend Table:
// database/tables.dart
import 'package:drift/drift.dart';
// Tabel Kategori
class Kategoris extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get nama => text().withLength(min: 1, max: 100)();
TextColumn get deskripsi => text().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
// Tabel Produk
class Produks extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get nama => text().withLength(min: 1, max: 255)();
RealColumn get harga => real()();
BoolColumn get tersedia => boolean().withDefault(const Constant(true))();
IntColumn get stok => integer().withDefault(const Constant(0))();
// Foreign key ke Kategoris
IntColumn get kategoriId => integer().references(Kategoris, #id)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().nullable()();
}
// Tabel Pesanan
class Pesanans extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get nomorPesanan => text().unique()();
RealColumn get total => real()();
TextColumn get status => text().withDefault(const Constant('menunggu'))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
// Tabel junction untuk relasi many-to-many: Pesanan ↔ Produk
class PesananProduks extends Table {
IntColumn get pesananId => integer().references(Pesanans, #id)();
IntColumn get produkId => integer().references(Produks, #id)();
IntColumn get jumlah => integer()();
RealColumn get hargaSatuan => real()();
@override
Set<Column> get primaryKey => {pesananId, produkId};
}
Database Class #
// database/app_database.dart
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
part 'app_database.g.dart'; // file generated
@DriftDatabase(tables: [Kategoris, Produks, Pesanans, PesananProduks])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1; // increment saat schema berubah
static QueryExecutor _openConnection() {
return driftDatabase(name: 'app_database');
}
}
// main.dart -- buat satu instance database
late AppDatabase database;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
database = AppDatabase();
runApp(const MyApp());
}
# Generate kode
flutter pub run build_runner build --delete-conflicting-outputs
Operasi CRUD Dasar #
Drift men-generate class data (misal Produk) dan companion class (misal ProduksCompanion) untuk setiap tabel:
// INSERT
Future<int> tambahProduk({
required String nama,
required double harga,
required int kategoriId,
}) async {
return database.into(database.produks).insert(
ProduksCompanion.insert(
nama: nama,
harga: harga,
kategoriId: kategoriId,
// tersedia, stok: opsional (punya default)
),
);
}
// INSERT OR REPLACE -- upsert
await database.into(database.produks).insertOnConflictUpdate(
ProduksCompanion.insert(nama: 'Produk A', harga: 10000, kategoriId: 1),
);
// SELECT SEMUA
Future<List<Produk>> semuaProduk() async {
return database.select(database.produks).get();
}
// SELECT DENGAN FILTER
Future<List<Produk>> produkTersedia() async {
return (database.select(database.produks)
..where((p) => p.tersedia.equals(true))
..orderBy([(p) => OrderingTerm.asc(p.nama)]))
.get();
}
// SELECT SATU
Future<Produk?> produkById(int id) async {
return (database.select(database.produks)
..where((p) => p.id.equals(id)))
.getSingleOrNull();
}
// UPDATE
Future<void> updateHarga(int id, double hargaBaru) async {
await (database.update(database.produks)
..where((p) => p.id.equals(id)))
.write(ProduksCompanion(harga: Value(hargaBaru)));
}
// UPDATE dengan Companion lengkap
await (database.update(database.produks)
..where((p) => p.id.equals(1)))
.write(ProduksCompanion(
nama: const Value('Nama Baru'),
harga: const Value(99000),
updatedAt: Value(DateTime.now()),
));
// DELETE
Future<void> hapusProduk(int id) async {
await (database.delete(database.produks)
..where((p) => p.id.equals(id)))
.go();
}
Reactive Stream — watch() #
Ganti .get() dengan .watch() untuk mendapatkan Stream yang auto-update:
// Stream semua produk -- auto-emit saat ada perubahan di tabel produks
Stream<List<Produk>> watchProduk() {
return (database.select(database.produks)
..where((p) => p.tersedia.equals(true))
..orderBy([(p) => OrderingTerm.asc(p.harga)]))
.watch();
}
// Gunakan di widget dengan StreamBuilder
StreamBuilder<List<Produk>>(
stream: database.watchProduk(),
builder: (context, snapshot) {
if (snapshot.hasError) return ErrorView(pesan: snapshot.error.toString());
if (!snapshot.hasData) return const CircularProgressIndicator();
return ProdukList(produk: snapshot.data!);
},
)
// Atau dengan Riverpod StreamProvider
final produkStreamProvider = StreamProvider<List<Produk>>((ref) {
return ref.watch(databaseProvider).watchProduk();
});
JOIN — Query Antar Tabel #
// Data class untuk hasil JOIN
class ProdukDenganKategori {
final Produk produk;
final Kategori kategori;
ProdukDenganKategori(this.produk, this.kategori);
}
// JOIN antara Produks dan Kategoris
Future<List<ProdukDenganKategori>> produkDenganKategori() async {
final query = database.select(database.produks).join([
innerJoin(
database.kategoris,
database.kategoris.id.equalsExp(database.produks.kategoriId),
),
]);
final rows = await query.get();
return rows.map((row) {
return ProdukDenganKategori(
row.readTable(database.produks),
row.readTable(database.kategoris),
);
}).toList();
}
// JOIN dengan filter dan sort
Future<List<ProdukDenganKategori>> produkElektronik() async {
final query = (database.select(database.produks)
..where((p) => p.tersedia.equals(true)))
.join([
innerJoin(
database.kategoris,
database.kategoris.id.equalsExp(database.produks.kategoriId),
),
])
..where(database.kategoris.nama.equals('Elektronik'))
..orderBy([OrderingTerm.asc(database.produks.harga)]);
final rows = await query.get();
return rows.map((row) => ProdukDenganKategori(
row.readTable(database.produks),
row.readTable(database.kategoris),
)).toList();
}
DAO Pattern — Kelompokkan Query per Domain #
DAO (Data Access Object) memisahkan query per domain — membuat kode lebih terorganisir dan mudah ditest:
// database/daos/produk_dao.dart
part of '../app_database.dart';
@DriftAccessor(tables: [Produks, Kategoris])
class ProdukDao extends DatabaseAccessor<AppDatabase> with _$ProdukDaoMixin {
ProdukDao(super.db);
// Semua query terkait produk di sini
Future<List<Produk>> getAll() => select(produks).get();
Stream<List<Produk>> watchAll() => select(produks).watch();
Stream<List<Produk>> watchTersedia() {
return (select(produks)
..where((p) => p.tersedia.equals(true))
..orderBy([(p) => OrderingTerm.asc(p.nama)]))
.watch();
}
Future<Produk?> getById(int id) {
return (select(produks)..where((p) => p.id.equals(id))).getSingleOrNull();
}
Future<int> insert(ProduksCompanion companion) {
return into(produks).insert(companion);
}
Future<void> update(ProduksCompanion companion) {
return (update(produks)..where((p) => p.id.equals(companion.id.value)))
.write(companion);
}
Future<void> delete(int id) {
return (delete(produks)..where((p) => p.id.equals(id))).go();
}
Future<int> countKategori(int kategoriId) async {
final count = produks.id.count();
final query = selectOnly(produks)
..addColumns([count])
..where(produks.kategoriId.equals(kategoriId));
final row = await query.getSingle();
return row.read(count) ?? 0;
}
}
// Daftarkan DAO di AppDatabase
@DriftDatabase(tables: [Kategoris, Produks, Pesanans, PesananProduks], daos: [ProdukDao])
class AppDatabase extends _$AppDatabase {
ProdukDao get produkDao => ProdukDao(this);
// ...
}
Migrasi Schema #
Saat schema berubah (tambah kolom, tabel baru), increment schemaVersion dan definisikan migrasi:
@DriftDatabase(tables: [Kategoris, Produks, Pesanans, PesananProduks])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 3; // increment dari 2 ke 3
@override
MigrationStrategy get migration {
return MigrationStrategy(
// Dipanggil saat database dibuat pertama kali
onCreate: (m) async {
await m.createAll();
},
// Dipanggil saat schemaVersion berubah
onUpgrade: (m, from, to) async {
if (from < 2) {
// Migrasi dari versi 1 ke 2: tambah kolom stok
await m.addColumn(produks, produks.stok);
}
if (from < 3) {
// Migrasi dari versi 2 ke 3: tambah tabel PesananProduks
await m.createTable(pesananProduks);
}
},
// Opsional: validasi setelah migrasi
beforeOpen: (details) async {
if (details.wasCreated) {
// Seed data awal jika perlu
await _seedData();
}
// Aktifkan foreign keys
await customStatement('PRAGMA foreign_keys = ON');
},
);
}
Future<void> _seedData() async {
await into(kategoris).insert(
KategorisCompanion.insert(nama: 'Umum'),
);
}
}
Transaksi #
// Transaksi: semua berhasil atau semua dibatalkan
Future<void> buatPesananDenganItem(
Pesanan pesanan,
List<({Produk produk, int jumlah})> items,
) async {
await database.transaction(() async {
// 1. Simpan pesanan
final pesananId = await database.into(database.pesanans).insert(
PesanansCompanion.insert(
nomorPesanan: pesanan.nomorPesanan,
total: pesanan.total,
),
);
// 2. Simpan setiap item pesanan
for (final item in items) {
await database.into(database.pesananProduks).insert(
PesananProduks.insert(
pesananId: pesananId,
produkId: item.produk.id,
jumlah: item.jumlah,
hargaSatuan: item.produk.harga,
),
);
// 3. Kurangi stok
await (database.update(database.produks)
..where((p) => p.id.equals(item.produk.id)))
.write(ProduksCompanion(
stok: Value(item.produk.stok - item.jumlah),
));
}
// Jika ada error di sini -- semua di atas di-rollback otomatis
});
}
Integrasi Riverpod #
// Provider
final databaseProvider = Provider<AppDatabase>((ref) {
throw UnimplementedError('Initialize in main()');
});
final produkDaoProvider = Provider<ProdukDao>((ref) {
return ref.watch(databaseProvider).produkDao;
});
// StreamProvider untuk reactive data
final produkStreamProvider = StreamProvider<List<Produk>>((ref) {
return ref.watch(produkDaoProvider).watchAll();
});
// Notifier untuk CRUD
class ProdukNotifier extends AsyncNotifier<List<Produk>> {
@override
Future<List<Produk>> build() {
return ref.watch(produkDaoProvider).getAll();
}
Future<void> tambah(String nama, double harga, int kategoriId) async {
await ref.read(produkDaoProvider).insert(
ProduksCompanion.insert(
nama: nama, harga: harga, kategoriId: kategoriId,
),
);
ref.invalidateSelf();
}
}
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final db = AppDatabase();
runApp(ProviderScope(
overrides: [databaseProvider.overrideWithValue(db)],
child: const MyApp(),
));
}
Ringkasan #
- Drift adalah ORM type-safe untuk SQLite — query diperiksa saat compile time, bukan saat runtime. Ini mencegah banyak bug yang umum terjadi dengan raw SQL.
- Definisikan tabel sebagai class yang meng-extend
Tabledengan column builder yang fluent (text(),integer(),dateTime(), dll.).- Ganti
.get()dengan.watch()untuk mendapatkan Stream reaktif — widget otomatis rebuild saat data berubah tanpa kode tambahan.- JOIN antar tabel menggunakan
.join([innerJoin(...)])— hasilnya type-safe dan bisa di-mapping ke custom data class.- DAO Pattern mengelompokkan query per domain — lebih terorganisir, mudah ditest dengan in-memory database, dan mencegah database class membengkak.
- Migrasi schema dikelola di
MigrationStrategy— incrementschemaVersiondan definisikan langkah migrasi dionUpgrade. Data pengguna tetap aman.- Transaksi dengan
database.transaction(() async {...})— jika ada exception di dalam, semua perubahan di-rollback otomatis.- Drift mendukung semua platform termasuk Flutter Web (dengan WASM) — berbeda dari ObjectBox yang tidak support web.