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 Table dengan 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 — increment schemaVersion dan definisikan langkah migrasi di onUpgrade. 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.

← Sebelumnya: ObjectBox   Berikutnya: Best Practice →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact