Flutter: Firestore CRUD
CRUD with Firebase Firestore in Flutter
Pengenalan
Firestore merupakan salah database NoSQL yang memiliki kemampuan hampir mirip seperti Firebase Realtime Database. Dari dokumentasinya disebutkan juga bahwa Firestore ini juga memiliki fitur untuk melakukan sinkron secara realtime dan tentunya mendukung untuk data offline juga. Yang membedakan Firestore dengan Firebase Realtime Database adalah dari segi kompleksitasnya. Firestore di-design dengan semaksimal mungkin agar database NoSQL ini memiliki kemampuan yang lebih kompleks contohnya seperti, melakukan query yang super rumit dan daya penyimpanan yang lebih kompleks. Lalu, untuk cara kerjanya sendiri juga agak sedikit berbeda dengan Firebase Realtime Database dimana, jikalau di Firebase Realtime Database kita hanya menyimpan data kita dalam bentuk sebuah pohon JSON maka, di Firestore kita lebih dipermudah karena kita menggunakan konsep collections. Lalu, didalam collections tersebut kita bisa menyimpan documents dan didalam documents kita bisa menyimpan data.
Jika kamu bingung memilih apakah mau menggunakan Realtime Database atau Firestore maka, kamu bisa kunjungi link berikut untuk melihat info lebih lengkapnya.
Contoh Projek
Buat Projek Flutter
Pertama-tama kita buat projek Flutter dengan nama flutter_firestore_todo di IDE atau Text Editor favorit kita.
Ubah file pubspec.yaml
Kemudian, kita buka file pubspec.yaml dan ubah menjadi seperti berikut pada bagian dependencies.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
# Flutter plugin for Cloud Firestore, a cloud hosted, noSQL database with live synchronization
# and offline support on Android and iOS.
cloud_firestore: ^0.13.2+1
# Plugin for provides internationalization and localization, facilities, including message
# translation, plurals and genders, date/number formatting and parsing, and bidirectional text
intl: ^0.16.1
Kemudian, jalankan perintah flutter packages get
untuk mengunduh paket-paket tersebut.
Buat Projek Firebase
Untuk langkah ini saya anggap kita semua sudah tahu cara buat projek di Firebase. Pada contoh kali ini saya coba mencontohkan untuk Android dan Web saja ya. Karena, untuk iOS berdasarkan saat tulisan ini dipublikasikan ternyata masih ada error ketika di-build ke iOS. Berikut issue-nya di Github.
Persiapan Android
Pada firebase console-nya, silakan kita buat projek untuk Android
Kemudian, silakan isi form yang ada dan unduh file google-services.json dan letakkan kedalam projek kita pada direktori android/app.
Persiapan Web
Untuk Web, silakan buat projek sama seperti pada langkah persiapan Android hanya saja pilih yang Web lalu, isi form yang ada. Kemudian, buka file index.html dan masukkan kode berikut.
Untuk nilai firebaseConfig kita bisa mengisinya sesuai dengan milik kita masing-masing di projek Firebase yang sudah kita buat tadi. Cara melihatnya dari menu Settings dan pilih aplikasi Web yang sudah kita tambahkan diawal tadi.
Catatan: pada tulisan ini dipublikasikan saya menggunakan Flutter Channel Beta dengan versi 1.14.6 dan Flutter Web juga masih ada di channel Beta.
Buat File Pendukung
Sebelum kita membuat tampilannya ada beberapa file pendukung yang perlu kita buat pada contoh projek kali ini yaitu sebagai berikut.
- app_color.dart
- widget_background.dart
Untuk yang pertama, silakan buat file baru dengan nama app_color.dart dan masukkan kode berikut.
import 'package:flutter/material.dart';
class AppColor {
final Color colorPrimary = Color(0xFFFBE4D4);
final Color colorSecondary = Color(0xFFEC81B7);
final Color colorTertiary = Color(0xFF425195);
}
Untuk yang berikutnya, kita buat file baru dengan nama widget_background.dart dan masukkan kode berikut.
import 'package:flutter/material.dart';
import 'package:flutter_firestore_todo/app_color.dart';
class WidgetBackground extends StatelessWidget {
final AppColor appColor = AppColor();
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: -64,
right: -128,
child: Container(
width: 256.0,
height: 256.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9000),
color: appColor.colorTertiary,
),
),
),
Positioned(
top: -164,
right: -8.0,
child: Container(
width: 256.0,
height: 256.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9000),
backgroundBlendMode: BlendMode.hardLight,
color: Colors.redAccent.withOpacity(0.8),
),
),
)
],
);
}
}
Buat Tampilan Utama
Pada tampilan utamanya kita akan membuat tampilan seperti berikut.
Untuk membuatnya silakan buka file main.dart dan isikan dengan kode berikut.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_firestore_todo/app_color.dart';
import 'package:flutter_firestore_todo/widget_background.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: AppColor().colorSecondary,
),
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final GlobalKey<ScaffoldState> scaffoldState = GlobalKey<ScaffoldState>();
final Firestore firestore = Firestore.instance;
final AppColor appColor = AppColor();
@override
Widget build(BuildContext context) {
MediaQueryData mediaQueryData = MediaQuery.of(context);
double widthScreen = mediaQueryData.size.width;
double heightScreen = mediaQueryData.size.height;
return Scaffold(
key: scaffoldState,
backgroundColor: appColor.colorPrimary,
body: SafeArea(
child: Stack(
children: <Widget>[
WidgetBackground(),
_buildWidgetListTodo(widthScreen, heightScreen, context),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(
Icons.add,
color: Colors.white,
),
onPressed: () async {
// TODO: fitur tambah task
},
backgroundColor: appColor.colorTertiary,
),
);
}
Container _buildWidgetListTodo(double widthScreen, double heightScreen, BuildContext context) {
return Container(
width: widthScreen,
height: heightScreen,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
child: Text(
'Todo List',
style: Theme.of(context).textTheme.title,
),
),
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: firestore.collection('tasks').orderBy('date').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
padding: EdgeInsets.all(8.0),
itemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index) {
DocumentSnapshot document = snapshot.data.documents[index];
Map<String, dynamic> task = document.data;
String strDate = task['date'];
return Card(
child: ListTile(
title: Text(task['name']),
subtitle: Text(
task['description'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
isThreeLine: false,
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 24.0,
height: 24.0,
decoration: BoxDecoration(
color: appColor.colorSecondary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'${int.parse(strDate.split(' ')[0])}',
style: TextStyle(color: Colors.white),
),
),
),
SizedBox(height: 4.0),
Text(
strDate.split(' ')[1],
style: TextStyle(fontSize: 12.0),
),
],
),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) {
return List<PopupMenuEntry<String>>()
..add(PopupMenuItem<String>(
value: 'edit',
child: Text('Edit'),
))
..add(PopupMenuItem<String>(
value: 'delete',
child: Text('Delete'),
));
},
onSelected: (String value) async {
if (value == 'edit') {
// TODO: fitur edit task
} else if (value == 'delete') {
// TODO: fitur hapus task
}
},
child: Icon(Icons.more_vert),
),
),
);
},
);
},
),
),
],
),
);
}
}
Pada kode diatas bisa kita lihat bahwa ada beberapa bagian kode yang masih kita beri komentar // TODO:
. Kode-kode ini nantinya akan kita lengkapi pada langkah berikutnya ya 😉.
Bedah Kode main.dart
Didalam file main.dart ada beberapa kode yang akan saya jelaskan mengenai fungsi si Firestore untuk mengambil data. Fungsi tersebut bisa kita lihat pada kode berikut.
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: firestore.collection('tasks').orderBy('date').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
padding: EdgeInsets.all(8.0),
itemCount: snapshot.data.documents.length,
itemBuilder: (BuildContext context, int index) {
DocumentSnapshot document = snapshot.data.documents[index];
Map<String, dynamic> task = document.data;
String strDate = task['date'];
return Card(
child: ListTile(
title: Text(task['name']),
subtitle: Text(
task['description'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
isThreeLine: false,
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 24.0,
height: 24.0,
decoration: BoxDecoration(
color: appColor.colorSecondary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'${int.parse(strDate.split(' ')[0])}',
style: TextStyle(color: Colors.white),
),
),
),
SizedBox(height: 4.0),
Text(
strDate.split(' ')[1],
style: TextStyle(fontSize: 12.0),
),
],
),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) {
return List<PopupMenuEntry<String>>()
..add(PopupMenuItem<String>(
value: 'edit',
child: Text('Edit'),
))
..add(PopupMenuItem<String>(
value: 'delete',
child: Text('Delete'),
));
},
onSelected: (String value) async {
if (value == 'edit') {
// TODO: fitur edit task
} else if (value == 'delete') {
// TODO: fitur hapus task
}
},
child: Icon(Icons.more_vert),
),
),
);
},
);
},
),
)
Pada kode firestore.collection('tasks').orderBy('date').snapshots()
ini berfungsi untuk mengambil data koleksi yang bernama tasks dari Firestore lalu mengurutkannya berdasarkan nilai dari key date.
Buat Fitur Menambahkan Task
Untuk membuat fitur tambah task kita perlu buat class baru dengan nama create_task_screen.dart dan masukkan kode berikut.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_firestore_todo/app_color.dart';
import 'package:flutter_firestore_todo/widget_background.dart';
import 'package:intl/intl.dart';
class CreateTaskScreen extends StatefulWidget {
final bool isEdit;
final String documentId;
final String name;
final String description;
final String date;
CreateTaskScreen({
@required this.isEdit,
this.documentId = '',
this.name = '',
this.description = '',
this.date = '',
});
@override
_CreateTaskScreenState createState() => _CreateTaskScreenState();
}
class _CreateTaskScreenState extends State<CreateTaskScreen> {
final GlobalKey<ScaffoldState> scaffoldState = GlobalKey<ScaffoldState>();
final Firestore firestore = Firestore.instance;
final AppColor appColor = AppColor();
final TextEditingController controllerName = TextEditingController();
final TextEditingController controllerDescription = TextEditingController();
final TextEditingController controllerDate = TextEditingController();
double widthScreen;
double heightScreen;
DateTime date = DateTime.now().add(Duration(days: 1));
bool isLoading = false;
@override
void initState() {
if (widget.isEdit) {
date = DateFormat('dd MMMM yyyy').parse(widget.date);
controllerName.text = widget.name;
controllerDescription.text = widget.description;
controllerDate.text = widget.date;
} else {
controllerDate.text = DateFormat('dd MMMM yyyy').format(date);
}
super.initState();
}
@override
Widget build(BuildContext context) {
MediaQueryData mediaQueryData = MediaQuery.of(context);
widthScreen = mediaQueryData.size.width;
heightScreen = mediaQueryData.size.height;
return Scaffold(
key: scaffoldState,
backgroundColor: appColor.colorPrimary,
resizeToAvoidBottomInset: false,
body: SafeArea(
child: Stack(
children: <Widget>[
WidgetBackground(),
Container(
width: widthScreen,
height: heightScreen,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildWidgetFormPrimary(),
SizedBox(height: 16.0),
_buildWidgetFormSecondary(),
isLoading
? Container(
color: Colors.white,
padding: const EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(appColor.colorTertiary),
),
),
)
: _buildWidgetButtonCreateTask(),
],
),
),
],
),
),
);
}
Widget _buildWidgetFormPrimary() {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Icon(
Icons.arrow_back,
color: Colors.grey[800],
),
),
SizedBox(height: 16.0),
Text(
widget.isEdit ? 'Edit\nTask' : 'Create\nNew Task',
style: Theme.of(context).textTheme.display1.merge(
TextStyle(color: Colors.grey[800]),
),
),
SizedBox(height: 16.0),
TextField(
controller: controllerName,
decoration: InputDecoration(
labelText: 'Name',
),
style: TextStyle(fontSize: 18.0),
),
],
),
);
}
Widget _buildWidgetFormSecondary() {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24.0),
topRight: Radius.circular(24.0),
),
),
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
children: <Widget>[
TextField(
controller: controllerDescription,
decoration: InputDecoration(
labelText: 'Description',
suffixIcon: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Icon(Icons.description),
],
),
),
style: TextStyle(fontSize: 18.0),
),
SizedBox(height: 16.0),
TextField(
controller: controllerDate,
decoration: InputDecoration(
labelText: 'Date',
suffixIcon: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Icon(Icons.today),
],
),
),
style: TextStyle(fontSize: 18.0),
readOnly: true,
onTap: () async {
DateTime today = DateTime.now();
DateTime datePicker = await showDatePicker(
context: context,
initialDate: date,
firstDate: today,
lastDate: DateTime(2021),
);
if (datePicker != null) {
date = datePicker;
controllerDate.text = DateFormat('dd MMMM yyyy').format(date);
}
},
),
],
),
),
);
}
Widget _buildWidgetButtonCreateTask() {
return Container(
width: double.infinity,
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: RaisedButton(
color: appColor.colorTertiary,
child: Text(widget.isEdit ? 'UPDATE TASK' : 'CREATE TASK'),
textColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.0),
),
onPressed: () async {
String name = controllerName.text;
String description = controllerDescription.text;
String date = controllerDate.text;
if (name.isEmpty) {
_showSnackBarMessage('Name is required');
return;
} else if (description.isEmpty) {
_showSnackBarMessage('Description is required');
return;
}
setState(() => isLoading = true);
if (widget.isEdit) {
DocumentReference documentTask = firestore.document('tasks/${widget.documentId}');
firestore.runTransaction((transaction) async {
DocumentSnapshot task = await transaction.get(documentTask);
if (task.exists) {
await transaction.update(
documentTask,
<String, dynamic>{
'name': name,
'description': description,
'date': date,
},
);
Navigator.pop(context, true);
}
});
} else {
CollectionReference tasks = firestore.collection('tasks');
DocumentReference result = await tasks.add(<String, dynamic>{
'name': name,
'description': description,
'date': date,
});
if (result.documentID != null) {
Navigator.pop(context, true);
}
}
},
),
);
}
void _showSnackBarMessage(String message) {
scaffoldState.currentState.showSnackBar(SnackBar(
content: Text(message),
));
}
}
Kemudian, kita buka kembali file main.dart dan ubah komentar // TODO: fitur tambah task
menjadi kode berikut.
bool result = await Navigator.push(context, MaterialPageRoute(builder: (context) => CreateTaskScreen(isEdit: false)));
if (result != null && result) {
scaffoldState.currentState.showSnackBar(SnackBar(
content: Text('Task has been created'),
));
setState(() {});
}
Selanjutnya, coba jalankan kembali programnya dan tap floating action button tambah. Maka, si pengguna akan diarahkan ke tampilan form seperti gambar berikut.
Sekarang coba kita test masukkan data pada form tambah task diatas. Jika berhasil seharusnya datanya akan masuk ke Firestore.
Bedah Kode create_task_screen.dart
Pada file create_task_screen.dart hanya ada satu fungsi yang kita gunakan pada Firestore yaitu, untuk menambahkan data kedalam Firestore. Berikut ialah kode yang berfungsi untuk menambahkan data kedalam Firestore.
CollectionReference tasks = firestore.collection('tasks');
DocumentReference result = await tasks.add(<String, dynamic>{
'name': name,
'description': description,
'date': date,
});
Awalnya kita buat terlebih dahulu objek CollectionReference
-nya berdasarkan nama collection yang akan kita simpan. Pada contoh kali ini nama collection-nya adalah tasks. Kemudian, kita masukkan data yang ingin kita tambahkan kedalam collection tersebut dengan cara menggunakan fungsi add
yang disediakan oleh objek CollectionReference
. Adapun data yang kita masukkan kedalam collection tasks adalah name, description, dan date.
Buat Fitur Hapus Task
Untuk membuat fitur hapus task silakan kita buka kembali file main.dart dan ubah kode berikut.
// TODO: fitur hapus task
Menjadi seperti berikut.
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Are You Sure'),
content: Text('Do you want to delete ${task['name']}?'),
actions: <Widget>[
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.pop(context);
},
),
FlatButton(
child: Text('Delete'),
onPressed: () {
document.reference.delete();
Navigator.pop(context);
setState(() {});
},
),
],
);
},
);
Coba jalankan kembali program diatas dan tap icon more lalu, pilih Delete. Selanjutnya, akan muncul dialog konfirmasi dan silakan pilih Delete. Jika berhasil seharusnya data yang dihapus akan hilang dari list todo.
Bedah Kode Fitur Hapus Task
Untuk menghapus sebuah document dari Firestore kita bisa menggunakan fungsi document.reference.delete()
dimana, document merupakan objek dari DocumentSnapshot
yang mana objek tersebut merupakan data document yang akan kita hapus.
Buat Fitur Edit Task
Untuk membuat fitur edit task silakan buka file main.dart dan ubah kode berikut.
// TODO: fitur edit task
Menjadi seperti berikut.
bool result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return CreateTaskScreen(
isEdit: true,
documentId: document.documentID,
name: task['name'],
description: task['description'],
date: task['date'],
);
}),
);
if (result != null && result) {
scaffoldState.currentState.showSnackBar(SnackBar(
content: Text('Task has been updated'),
));
setState(() {});
}
Lalu, buka kembali file create_task_screen.dart dan lihat didalam property onPressed
dari widget RaisedButton
dimana, didalamnya ada kode berikut.
if (widget.isEdit) {
DocumentReference documentTask = firestore.document('tasks/${widget.documentId}');
firestore.runTransaction((transaction) async {
DocumentSnapshot task = await transaction.get(documentTask);
if (task.exists) {
await transaction.update(
documentTask,
<String, dynamic>{
'name': name,
'description': description,
'date': date,
},
);
Navigator.pop(context, true);
}
});
}
Kode tersebutlah yang berfungsi untuk melakukan update data ke Firestore. Untuk mengetesnya, sekarang coba jalankan lagi programnya dan tap icon more lalu, pilih Edit. Setelah itu, kita akan diarahkan ke form task untuk melakukan perubahan data. Dan jika sudah diubah silakan tap button Update Task.
Bedah Kode Fitur Edit Task
Untuk melakukan perubahan data document di Firestore kita bisa menggunakan fungsi transaction
dimana, konsep transaction
ini adalah kita menjalankan sebuah fungsi secara sequence atau berurutan. Didalam transaction
tersebut kita ambil dahulu data document berdasarkan document ID yang kita dapatkan dari list todo. Jikalau datanya tersedia maka, lakukan fungsi transaction.update()
. Berikut adalah kodenya yang berfungsi untuk melakukan edit data di Firestore.
DocumentReference documentTask = firestore.document('tasks/${widget.documentId}');
firestore.runTransaction((transaction) async {
DocumentSnapshot task = await transaction.get(documentTask);
if (task.exists) {
await transaction.update(
documentTask,
<String, dynamic>{
'name': name,
'description': description,
'date': date,
},
);
Navigator.pop(context, true);
}
});
Kesimpulan
Jadi, dari tulisan ini saya ambil kesimpulan bahwa penggunaan Firestore sebagai MBaaS (Mobile Backend as a Service) merupakan pilihan yang tepat bagi kita yang mau mengembangkan aplikasi tanpa harus memikirkan masalah infrastruktur backend-nya. Selain itu, karena Flutter sudah mendukung untuk Mobile (Android & iOS) dan Web jadi, saya rasa ini adalah kombinasi yang benar-benar sempurna bagi kita karena, dengan single codebase-nya kita sudah bisa membuat 3 aplikasi yang berbeda platform secara bersamaan. Ini jelas-jelas menghemat biaya dan waktu pengembangan kita. Untuk source code projeknya bisa dilihat di Github ya.