Flutter
Firestore CRUD untuk Pemula
Tutorial CRUD Cloud Firestore di Flutter: model Task, TaskFirestoreService, create, read, stream realtime, update, delete, query status, sorting, dan error handling.
Firestore CRUD untuk Pemula
Materi ini adalah praktik setelah memahami Cloud Firestore Dasar. Kita akan membuat service untuk melakukan CRUD task di Firestore.
CRUD:
- Create: menambah data
- Read: membaca data
- Update: mengubah data
- Delete: menghapus data
Target:
- install
cloud_firestore - membuat model
Task - membuat
TaskFirestoreService - create task
- read task sekali
- stream task realtime
- update task
- delete task
- query berdasarkan status
- sorting berdasarkan
createdAt - error handling sederhana
1. Persiapan
Pastikan sudah:
- membuat Firebase project
- menghubungkan Flutter ke Firebase
- menjalankan
flutterfire configure - punya
firebase_options.dart - inisialisasi Firebase di
main.dart
Package yang dibutuhkan:
flutter pub add firebase_core
flutter pub add cloud_firestore
Import:
import 'package:cloud_firestore/cloud_firestore.dart';
2. Struktur Folder
Struktur sederhana:
lib/
models/
task.dart
services/
task_firestore_service.dart
pages/
firestore_task_page.dart
Kita mulai dari service layer dulu.
Nanti alurnya:
UI
-> Repository
-> TaskFirestoreService
-> Cloud Firestore
Tapi untuk materi ini:
UI
-> TaskFirestoreService
-> Cloud Firestore
Supaya konsep CRUD lebih mudah dipahami.
3. Struktur Data Firestore
Untuk latihan awal, pakai collection:
tasks/{taskId}
Contoh document:
tasks
abc123
title: "Belajar Firestore"
description: "Membuat CRUD"
status: "todo"
createdAt: timestamp
updatedAt: timestamp
Nanti setelah digabung dengan Firebase Auth, struktur akan berubah menjadi:
users/{uid}/tasks/{taskId}
Untuk sekarang, fokus dulu pada CRUD dasar.
4. Membuat Model Task
Buat file:
lib/models/task.dart
Isi:
import 'package:cloud_firestore/cloud_firestore.dart';
enum TaskStatus {
todo,
progress,
done,
}
extension TaskStatusLabel on TaskStatus {
String get label {
switch (this) {
case TaskStatus.todo:
return 'Todo';
case TaskStatus.progress:
return 'Progress';
case TaskStatus.done:
return 'Done';
}
}
}
class Task {
final String id;
final String title;
final String description;
final TaskStatus status;
final DateTime? createdAt;
final DateTime? updatedAt;
const Task({
required this.id,
required this.title,
required this.description,
required this.status,
this.createdAt,
this.updatedAt,
});
Map<String, dynamic> toCreateJson() {
return {
'title': title,
'description': description,
'status': status.name,
'createdAt': FieldValue.serverTimestamp(),
'updatedAt': FieldValue.serverTimestamp(),
};
}
Map<String, dynamic> toUpdateJson() {
return {
'title': title,
'description': description,
'status': status.name,
'updatedAt': FieldValue.serverTimestamp(),
};
}
factory Task.fromFirestore(
DocumentSnapshot<Map<String, dynamic>> doc,
) {
final data = doc.data();
if (data == null) {
throw Exception('Task data kosong');
}
return Task(
id: doc.id,
title: data['title'] as String,
description: data['description'] as String,
status: TaskStatus.values.byName(data['status'] as String),
createdAt: (data['createdAt'] as Timestamp?)?.toDate(),
updatedAt: (data['updatedAt'] as Timestamp?)?.toDate(),
);
}
Task copyWith({
String? id,
String? title,
String? description,
TaskStatus? status,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return Task(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
status: status ?? this.status,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
Penjelasan:
idberasal daridoc.id.toCreateJson()dipakai saat create data.toUpdateJson()dipakai saat update data.createdAthanya dibuat saat create.updatedAtberubah setiap update.Timestamp?dibuat nullable karena server timestamp bisa sementara kosong.
5. Kenapa Ada toCreateJson dan toUpdateJson
Saat create:
createdAt
updatedAt
perlu diisi.
Saat update:
createdAt
sebaiknya tidak diubah.
Karena itu kita pisahkan:
toCreateJson()
toUpdateJson()
Ini membuat data lebih rapi.
6. Membuat TaskFirestoreService
Buat file:
lib/services/task_firestore_service.dart
Isi awal:
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/task.dart';
class TaskFirestoreService {
final FirebaseFirestore _firestore;
TaskFirestoreService({
FirebaseFirestore? firestore,
}) : _firestore = firestore ?? FirebaseFirestore.instance;
CollectionReference<Map<String, dynamic>> get _tasksRef {
return _firestore.collection('tasks');
}
}
Penjelasan:
_firestoreadalah instance Firestore._tasksRefadalah reference ke collectiontasks.- Service dibuat agar kode Firestore tidak tersebar di UI.
7. Create Task
Tambahkan:
Future<void> createTask({
required String title,
required String description,
TaskStatus status = TaskStatus.todo,
}) async {
final task = Task(
id: '',
title: title,
description: description,
status: status,
);
await _tasksRef.add(task.toCreateJson());
}
Penggunaan:
await taskFirestoreService.createTask(
title: 'Belajar Firestore',
description: 'Membuat create task',
);
Firestore akan membuat document id otomatis.
8. Create Task dengan ID Sendiri
Jika ingin membuat id sendiri:
Future<void> setTask(Task task) async {
await _tasksRef.doc(task.id).set(task.toCreateJson());
}
Namun untuk pemula, add() lebih mudah.
Gunakan:
add()
jika Firestore boleh membuat id.
Gunakan:
doc(id).set()
jika id sudah ditentukan aplikasi.
9. Read Task Sekali
Membaca semua task sekali:
Future<List<Task>> getTasks() async {
final snapshot = await _tasksRef
.orderBy('createdAt', descending: true)
.get();
return snapshot.docs.map(Task.fromFirestore).toList();
}
Penggunaan:
final tasks = await taskFirestoreService.getTasks();
Kapan memakai get()?
- saat hanya perlu membaca sekali
- saat halaman tidak perlu realtime
- saat ingin refresh manual
10. Read Task Realtime
Membaca realtime:
Stream<List<Task>> watchTasks() {
return _tasksRef
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) {
return snapshot.docs.map(Task.fromFirestore).toList();
});
}
Penggunaan di UI:
StreamBuilder<List<Task>>(
stream: taskFirestoreService.watchTasks(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
final tasks = snapshot.data ?? [];
if (tasks.isEmpty) {
return const Center(
child: Text('Belum ada task'),
);
}
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text(task.title),
subtitle: Text(task.description),
);
},
);
},
)
Kapan memakai snapshots()?
- saat UI harus realtime
- saat data bisa berubah dari device lain
- saat ingin update otomatis
11. Read Task Berdasarkan ID
Future<Task?> getTaskById(String id) async {
final snapshot = await _tasksRef.doc(id).get();
if (!snapshot.exists) {
return null;
}
return Task.fromFirestore(snapshot);
}
Penggunaan:
final task = await taskFirestoreService.getTaskById(taskId);
Jika task tidak ada, return null.
12. Update Task
Future<void> updateTask(Task task) async {
await _tasksRef.doc(task.id).update(task.toUpdateJson());
}
Penggunaan:
await taskFirestoreService.updateTask(
task.copyWith(
title: 'Judul baru',
status: TaskStatus.done,
),
);
Catatan:
update() akan gagal jika document tidak ada.
13. Update Status
Jika hanya ingin update status:
Future<void> updateTaskStatus({
required String id,
required TaskStatus status,
}) async {
await _tasksRef.doc(id).update({
'status': status.name,
'updatedAt': FieldValue.serverTimestamp(),
});
}
Penggunaan:
await taskFirestoreService.updateTaskStatus(
id: task.id,
status: TaskStatus.done,
);
Ini lebih ringan daripada mengirim semua field.
14. Delete Task
Future<void> deleteTask(String id) async {
await _tasksRef.doc(id).delete();
}
Penggunaan:
await taskFirestoreService.deleteTask(task.id);
Jika document punya subcollection, delete document tidak otomatis menghapus subcollection. Untuk Task sederhana tanpa subcollection, aman.
15. Query Berdasarkan Status
Membaca task dengan status tertentu:
Future<List<Task>> getTasksByStatus(TaskStatus status) async {
final snapshot = await _tasksRef
.where('status', isEqualTo: status.name)
.orderBy('createdAt', descending: true)
.get();
return snapshot.docs.map(Task.fromFirestore).toList();
}
Realtime:
Stream<List<Task>> watchTasksByStatus(TaskStatus status) {
return _tasksRef
.where('status', isEqualTo: status.name)
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) {
return snapshot.docs.map(Task.fromFirestore).toList();
});
}
Catatan:
Query gabungan where + orderBy mungkin membutuhkan index. Jika Firestore memberi error index, ikuti link yang diberikan Firebase.
16. TaskFirestoreService Lengkap
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/task.dart';
class TaskFirestoreService {
final FirebaseFirestore _firestore;
TaskFirestoreService({
FirebaseFirestore? firestore,
}) : _firestore = firestore ?? FirebaseFirestore.instance;
CollectionReference<Map<String, dynamic>> get _tasksRef {
return _firestore.collection('tasks');
}
Future<void> createTask({
required String title,
required String description,
TaskStatus status = TaskStatus.todo,
}) async {
final task = Task(
id: '',
title: title,
description: description,
status: status,
);
await _tasksRef.add(task.toCreateJson());
}
Future<void> setTask(Task task) async {
await _tasksRef.doc(task.id).set(task.toCreateJson());
}
Future<List<Task>> getTasks() async {
final snapshot = await _tasksRef
.orderBy('createdAt', descending: true)
.get();
return snapshot.docs.map(Task.fromFirestore).toList();
}
Stream<List<Task>> watchTasks() {
return _tasksRef
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) {
return snapshot.docs.map(Task.fromFirestore).toList();
});
}
Future<Task?> getTaskById(String id) async {
final snapshot = await _tasksRef.doc(id).get();
if (!snapshot.exists) {
return null;
}
return Task.fromFirestore(snapshot);
}
Future<void> updateTask(Task task) async {
await _tasksRef.doc(task.id).update(task.toUpdateJson());
}
Future<void> updateTaskStatus({
required String id,
required TaskStatus status,
}) async {
await _tasksRef.doc(id).update({
'status': status.name,
'updatedAt': FieldValue.serverTimestamp(),
});
}
Future<void> deleteTask(String id) async {
await _tasksRef.doc(id).delete();
}
Future<List<Task>> getTasksByStatus(TaskStatus status) async {
final snapshot = await _tasksRef
.where('status', isEqualTo: status.name)
.orderBy('createdAt', descending: true)
.get();
return snapshot.docs.map(Task.fromFirestore).toList();
}
Stream<List<Task>> watchTasksByStatus(TaskStatus status) {
return _tasksRef
.where('status', isEqualTo: status.name)
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) {
return snapshot.docs.map(Task.fromFirestore).toList();
});
}
}
17. Contoh Halaman Test CRUD
Buat halaman sederhana untuk test.
class FirestoreTaskPage extends StatelessWidget {
FirestoreTaskPage({super.key});
final service = TaskFirestoreService();
Future<void> addSampleTask() async {
await service.createTask(
title: 'Belajar Firestore',
description: 'Membuat CRUD task',
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firestore Task'),
),
body: StreamBuilder<List<Task>>(
stream: service.watchTasks(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
final tasks = snapshot.data ?? [];
if (tasks.isEmpty) {
return const Center(
child: Text('Belum ada task'),
);
}
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text(task.title),
subtitle: Text(task.status.label),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () {
service.deleteTask(task.id);
},
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: addSampleTask,
child: const Icon(Icons.add),
),
);
}
}
Dengan halaman ini:
- tekan tombol
+untuk menambah task - task muncul otomatis karena stream
- tekan delete untuk menghapus task
18. Error Handling di Service
Untuk pembelajaran, service boleh sederhana. Namun untuk aplikasi lebih rapi, tambahkan error handling.
Contoh:
Future<void> createTask({
required String title,
required String description,
TaskStatus status = TaskStatus.todo,
}) async {
try {
final task = Task(
id: '',
title: title,
description: description,
status: status,
);
await _tasksRef.add(task.toCreateJson());
} on FirebaseException catch (error) {
throw Exception(error.message ?? 'Gagal membuat task');
}
}
Error umum:
- permission denied
- network error
- missing index
- data type mismatch
19. CRUD Per User
Setelah Firebase Auth dipakai, ubah collection menjadi per user.
CollectionReference<Map<String, dynamic>> userTasksRef(String uid) {
return _firestore
.collection('users')
.doc(uid)
.collection('tasks');
}
Create:
Future<void> createUserTask({
required String uid,
required String title,
required String description,
}) async {
await userTasksRef(uid).add({
'title': title,
'description': description,
'status': TaskStatus.todo.name,
'createdAt': FieldValue.serverTimestamp(),
'updatedAt': FieldValue.serverTimestamp(),
});
}
Stream:
Stream<List<Task>> watchUserTasks(String uid) {
return userTasksRef(uid)
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) {
return snapshot.docs.map(Task.fromFirestore).toList();
});
}
Ini akan dipakai di materi Task Manager Online per User.
20. Testing Manual
Lakukan test:
- Jalankan aplikasi.
- Tekan tambah task.
- Cek Firebase Console.
- Pastikan document muncul di collection
tasks. - Hapus task dari aplikasi.
- Pastikan document hilang di Firebase Console.
- Tambah task dari Firebase Console.
- Pastikan UI ikut update jika memakai stream.
- Ubah status task.
- Pastikan field status berubah.
21. Kesalahan Umum
Permission denied
Biasanya security rules menolak request.
Solusi sementara saat belajar:
- cek rules
- pastikan user login jika rules membutuhkan auth
- pastikan path sesuai rules
Timestamp null
Gunakan nullable:
(data['createdAt'] as Timestamp?)?.toDate()
Missing index
Query gabungan bisa butuh index.
Solusi:
- buka link dari error Firestore
- buat index di Firebase Console
Data type mismatch
Contoh:
data['title'] as String
akan error jika field title tidak ada atau bukan String.
Pastikan struktur data konsisten.
22. Checklist Firestore CRUD
Pastikan sudah bisa:
- Install
cloud_firestore. - Membuat model
Task. - Membuat
TaskFirestoreService. - Create task dengan
add. - Read task sekali dengan
get. - Read realtime dengan
snapshots. - Read task by id.
- Update task.
- Update status task.
- Delete task.
- Query task berdasarkan status.
- Sorting berdasarkan
createdAt. - Memahami
FieldValue.serverTimestamp. - Menangani
Timestamp?. - Memahami permission denied.
- Memahami konsep task per user.
Jika checklist ini aman, lanjut ke Task Manager Online per User.
23. Lanjutan Setelah Ini
Materi berikutnya:
- Task Manager Online per User.
- Repository dengan Firestore.
- Firestore Security Rules dasar.
- Sync local storage + Firestore.
Urutan yang disarankan:
Firestore CRUD
-> Task Manager Online per User
-> Security Rules
-> Offline-first sync