Flutter #23: SQLite & Drift Database — Local Storage yang Powerful
Halo! Di artikel sebelumnya kita sudah belajar SharedPreferences untuk menyimpan data sederhana. Tapi kalau kamu butuh menyimpan data yang lebih kompleks — seperti daftar tugas, catatan, atau data pengguna — SharedPreferences sudah tidak cukup.
Masalahnya, SharedPreferences cuma cocok untuk key-value sederhana. Kalau kamu butuh relasi antar data, query yang kompleks, atau menyimpan ratusan record dengan efisien, kamu butuh database asli.
Di sinilah SQLite masuk. Dan di Flutter, cara paling powerful untuk pakai SQLite adalah lewat Drift (sebelumnya bernama Moor).
Kenapa Drift, Bukan Langsung sqflite?
SQLite di Flutter sebenarnya bisa diakses langsung lewat package sqflite. Tapi ada beberapa masalah:
- Query ditulis sebagai string mentah — rentan typo
- Tidak ada type safety — error baru ketahuan saat runtime
- Tidak ada codegen — CRUD manual untuk setiap tabel
Drift menyelesaikan semua masalah ini:
- Type-safe queries — error ketahuan saat compile
- Code generation — tabel dan query otomatis dibuatkan
- Reactive streams — data berubah, UI otomatis update
- Migrasi database — lebih mudah dikelola
Yuk langsung praktek!
Setup Project
Tambahkan dependency ke pubspec.yaml:
dependencies:
drift: ^2.14.0
sqlite3_flutter_libs: ^0.5.0
path_provider: ^2.1.0
path: ^1.8.0
dev_dependencies:
drift_dev: ^2.14.0
build_runner: ^2.4.0
Lalu jalankan:
flutter pub get
dart run build_runner build --delete-conflicting-outputs
Definisi Database & Tabel
Buat file lib/database/app_database.dart:
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
// Import generated part file
part 'app_database.g.dart';
// Definisi tabel
class Tasks extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 100)();
TextColumn get description => text().nullable()();
BoolColumn get isDone => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
// Class database utama
@DriftDatabase(tables: [Tasks])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
// Versi schema — naikkan saat ada perubahan tabel
@override
int get schemaVersion => 1;
// CRUD Methods
Future<List<Task>> getAllTasks() => select(tasks).get();
Stream<List<Task>> watchAllTasks() => select(tasks).watch();
Future<int> insertTask(TasksCompanion task) => into(tasks).insert(task);
Future<bool> updateTask(Task task) => update(tasks).replace(task);
Future<int> deleteTask(int id) =>
(delete(tasks)..where((t) => t.id.equals(id))).go();
// Query: ambil task yang belum selesai
Stream<List<Task>> watchPendingTasks() =>
(select(tasks)..where((t) => t.isDone.equals(false))).watch();
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'tasks.sqlite'));
return NativeDatabase.createInBackground(file);
});
}
Jalankan code generator untuk membuat file .g.dart:
dart run build_runner build --delete-conflicting-outputs
Menggunakan Database di Widget
Sekarang kita pakai database di halaman utama:
import 'package:flutter/material.dart';
import 'package:drift/drift.dart' as drift;
import '../database/app_database.dart';
class TaskListPage extends StatefulWidget {
const TaskListPage({super.key});
@override
State<TaskListPage> createState() => _TaskListPageState();
}
class _TaskListPageState extends State<TaskListPage> {
late AppDatabase _database;
@override
void initState() {
super.initState();
_database = AppDatabase();
}
@override
void dispose() {
_database.close();
super.dispose();
}
void _addTask() {
final titleController = TextEditingController();
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Tugas Baru'),
content: TextField(
controller: titleController,
decoration: const InputDecoration(
hintText: 'Judul tugas...',
border: OutlineInputBorder(),
),
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Batal'),
),
FilledButton(
onPressed: () {
if (titleController.text.isNotEmpty) {
_database.insertTask(
TasksCompanion.insert(title: titleController.text),
);
}
Navigator.pop(ctx);
},
child: const Text('Simpan'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Daftar Tugas')),
floatingActionButton: FloatingActionButton(
onPressed: _addTask,
child: const Icon(Icons.add),
),
// StreamBuilder = reactive! UI update otomatis
body: StreamBuilder<List<Task>>(
stream: _database.watchAllTasks(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final tasks = snapshot.data!;
if (tasks.isEmpty) {
return const Center(
child: Text(
'Belum ada tugas.\nTap + untuk menambah!',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
);
}
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return CheckboxListTile(
value: task.isDone,
onChanged: (value) {
// Update status done
_database.updateTask(
task.copyWith(isDone: value ?? false),
);
},
title: Text(
task.title,
style: TextStyle(
decoration: task.isDone
? TextDecoration.lineThrough
: null,
color: task.isDone ? Colors.grey : null,
),
),
subtitle: task.description != null
? Text(task.description!)
: null,
secondary: IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _database.deleteTask(task.id),
),
);
},
);
},
),
);
}
}
Penjelasan Kunci
Type-Safe Queries
Perhatikan baris ini:
// Tidak perlu tulis string SQL manual!
Stream<List<Task>> watchPendingTasks() =>
(select(tasks)..where((t) => t.isDone.equals(false))).watch();
Drift menggunakan table dan column objects untuk membangun query. Kalau kamu salah nama kolom, error muncul saat compile — bukan saat user pakai aplikasi.
Reactive Streams dengan watch()
watchAllTasks() mengembalikan Stream. Setiap kali data di database berubah (insert, update, delete), stream otomatis emit data baru. StreamBuilder di UI akan otomatis rebuild.
Kamu tidak perlu refresh manual — semua reactif!
Migrasi Database
Ketika kamu menambah kolom atau tabel baru di masa depan, naikkan schemaVersion dan tambahkan migrasi:
@override
int get schemaVersion => 2; // Naikkan dari 1 ke 2
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(),
onUpgrade: (m, from, to) async {
// Tambah kolom 'priority' di tabel tasks
if (from < 2) {
await m.addColumn(tasks, tasks.priority);
}
},
);
Tips & Best Practice
- Inisialisasi sekali — buat instance
AppDatabase()di root app (biasanya dimain.dartatau Provider) dan bagikan ke semua widget - Pakai StreamBuilder untuk data yang sering berubah — reactive, otomatis update
- Pakai FutureBuilder untuk data yang jarang berubah (misal load data awal sekali)
- Pisahkan file database — jangan campur UI dan logic di satu file
- Gunakan Drift’s generated code — jangan menulis SQL mentah kecuali benar-benar perlu
Perbandingan: SharedPreferences vs Drift
| Aspek | SharedPreferences | Drift/SQLite |
|---|---|---|
| Data sederhana | ✅ Ideal | ⚠️ Overkill |
| Data kompleks | ❌ Tidak cocok | ✅ Ideal |
| Relasi antar data | ❌ Tidak ada | ✅ Full support |
| Query/filter | ❌ Manual | ✅ Powerful |
| Performance (banyak data) | ⚠️ Lambat | ✅ Sangat cepat |
| Type safety | ❌ Tipe campuran | ✅ Strongly typed |
Kapan Pakai Apa?
- SharedPreferences: token auth, settings ringan, theme preference
- Drift/SQLite: daftar kontak, catatan, riwayat, data offline, cache API kompleks
Ringkasan Hari Ini
- SQLite adalah database lokal yang powerful dan sudah built-in di Flutter
- Drift adalah package wrapper yang memberikan type safety, code generation, dan reactive streams
watch()mengembalikan stream — UI otomatis update saat data berubah- Migration strategy memudahkan upgrade schema tanpa kehilangan data
- Drift cocok untuk data kompleks, SharedPreferences untuk data sederhana
Di artikel berikutnya kita akan belajar Bottom Navigation & TabBar — bikin navigasi yang profesional di aplikasi Flutter!
Coba sendiri! Buat aplikasi todo list sederhana dengan Drift, lalu tambahkan fitur filter (selesai/belum). Share ke sosial media dan tag @ahsai001! 🚀