Tutorial Flutter — To Do List dengan SQLite

Belajar membuat To Do List dengan Flutter menggunakan Database SQLite

Jeremia Manogi Mario
Komunitas Android  CCIT-FTUI
10 min readDec 5, 2019

--

Halo semuanya, Jeremy disini dan kita kembali lagi di Medium Komandro. Kali ini saya akan membahas cara membuat Todo-list dengan SQLite. Pertama yang dibutuhkan adalah membuat file dart. File ini akan berisi kelas model atau kelas “penangkap”. Maksudnya apa? Jadi kelas akan dipanggil untuk menangkap input dari user. Setelah itu kita akan menyimpannya ke SQL.

Step dari kelas “penangkap” ini :

  1. Buat variable.

2. Buat constructor. Dengan parameter untuk menyimpan hasil inputnya di variable yang kita buat tadi.

3. Kita buat consturctor lagi. Tapi,dengan memberikan nama tambahan agar constructor pertama tidak menerima error.

Jika constructor ini dipanggil, akan mengambil data dari sql(Data dari sql yang tersimpan akan berbentuk Map) setelah itu, akan disimpan kembali ke dalam variable.

4. Kita membuat Getter dan setter.

getter akan mengambil nilai yang kita nanti dimasukkan ke consturctor dan setter ini akan dipakai untuk mengembalikan nilai yang dimasukkan dari constructor, per variable saja.

5. Terakhir, kita membuat method untuk memasukkan getter dan setter tadi ke dalam map

Kita akan memanggil method map ini untuk untuk membuat method update dan insert nanti.

Karena penjelasan kelas “penangkap” sudah, kita akan tulis semua variable yang kita butuhkan sekarang.

class ClassPenangkap {
int _id;
String _name;
String _phone;
ClassPenangkap(this._name, this._phone);ClassPenangkap.fromMap(Map<String, dynamic> map) {
this._id = map['id'];
this._name = map['name'];
this._phone = map['phone'];
}

int get id => _id;
String get name => _name;
String get phone => _phone;
set name(String value) {
_name = value;
}
set phone(String value) {
_phone = value;
}
Map<String, dynamic> toMap() {
Map<String, dynamic> map = Map<String, dynamic>();
map['id'] = this._id;
map['name'] = name;
map['phone'] = phone;
return map;
}
}

Next, Kita membuat kelas untuk memberikan akses supaya aplikasi yang kita buat dapat menggunakan database.

sebelum lanjut, tolong tambahkan sytax ini di pubspec.yaml .

sqflite: any
path_provider: any

Saya ingatkan, selalu perhatikan spasi dan enternya.Karena di file ini memperngaruhi.

Sekarang, kita buat kelas untuk mengakses databasenya.

Sekarang, tulis Syntax di bawah ini:

Future<Database> initDb() async {
Directory directory = await getApplicationDocumentsDirectory();
String path = directory.path + 'contact.db';
var todoDatabase = openDatabase(path, version: 1, onCreate: _createDb);
return todoDatabase;
}

Di method ini kita menggunakan Future untuk membuat method mengakses database.

APA ITU FUTURE?

Future adalah “tipe data” yang terpanggil dengan adanya delay atau “keterlambatan”. Tidak seperti method lainnya, sistem akan terus menjalankan method tersebut sampai method itu selesai berjalan. Contohnya ketika kita akan mengambil data yang ada di dalam database/API , kita membutuhkan method Future untuk mengambil data di dalam database/API tersebut. Untuk lebih jelasnya kalian bisa membuka sumber dibawah:

  1. https://medium.com/flutter-community/a-guide-to-using-futures-in-flutter-for-beginners-ebeddfbfb967
  2. https://www.youtube.com/watch?v=g9Uk1Xou0m4

Didalam flutter ada namanya async dan await.

async : menggunakan future pada sebuah method, dapat membuat sistem menunggu sampai terjadi Blocking. Makanya, method tersebut harus ditandai dengan async.

await : Jika ada method yang ditandai await, maka artinya sistem harus menunggu sampai syntax tersebut selesai berjalan.

Pada syntax di atas, Variable directory akan menunggu sampai method getApplicationDocumentsDirectory mengerjakan tugasnya. Method getApplicationDocumentsDirectory() berfungsi untuk mengambil direktori folder aplikasi untuk menempatkan data yang dibuat pengguna tidak dapat dibuat ulang oleh aplikasi Anda. Setelah itu kita gunakan variable String path,untuk membuat nama database kita dengan mengambil lokasi directory nya dan menambahkannya dengan nama database. Di medium kali ini saya akan menggunakan nama ‘bio.db’ .

Setelah itu kita baru membuat database dan akses untuk membuka database-nya dengan openDatabase. Dalam method ini akan membutuhkan nama database-nya, dengan variable path yang kita buat tadi. Lalu ada version dan onCreate.

version: bisa dikatakan version adalah level untuk penggunaan databasenya. Karena kita bahkan belum memiliki tablenya, kita cukup menulisnya ‘1’.

onCreate: bukan cuma onCreate, kita bisa menambahkanya dengan onConfigure,onUpgrade, dll. Tapi yang kita bahas hanya onCreate saja. Seperti namanya, fungsinya untuk membuat table supaya kita dapat mengaksesnya. Karena kita belum memiliki methodnya, mari kita buat.

dengan menambahkan Syntaxnya:

.................
void _createDb(Database db, int version) async {
await db.execute('''
CREATE TABLE contact (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
phone TEXT,
date TEXT
)
''');
}
...............

Setiap method yang akan di simpan pada onCreate atau “teman — temannya” harus menyisipkan parameter Database, dan parameter versi. Dan kemudian kita menggunakan method execute untuk mengeksekusi syntax SQL CREATE TABLE — nya.

Setelah itu, kita buat kelas untuk melakukan Operasi CRUD.

Kali ini tulis saja import di atas, karena ada beberapa kalimat yang “Ambigu ” dalam sistem .Dan tulis di dalamnya:

...................
static const todoTable = 'contact';
static const id = 'id';
static const name = 'name';
static const phone = 'phone';
AccesDatabase dbHelper = new AccesDatabase();
Future<int> insert(ClassPenangkap todo) async {
Database db = await dbHelper.initDb();
final sql = '''INSERT INTO ${CRUD.todoTable}
(
${CRUD.name},
${CRUD.phone}
)
VALUES (?,?)''';
List<dynamic> params = [todo.name, todo.phone];
final result = await db.rawInsert(sql, params);
return result;
}
Future<int> update(ClassPenangkap todo) async {
Database db = await dbHelper.initDb();
final sql = '''UPDATE ${CRUD.todoTable}
SET ${CRUD.name} = ?, ${CRUD.phone}
WHERE ${CRUD.id} = ?
''';
List<dynamic> params = [todo.name, todo.phone, todo.id];
final result = await db.rawUpdate(sql, params);
return result;
}
Future<int> delete(ClassPenangkap todo) async {
Database db = await dbHelper.initDb();
final sql = '''DELETE FROM ${CRUD.todoTable}
WHERE ${CRUD.id} = ?
''';
List<dynamic> params = [todo.id];
final result = await db.rawDelete(sql, params);
return result;
}
Future<List<ClassPenangkap>> getContactList() async {
Database db = await dbHelper.initDb();
final sql = '''SELECT * FROM ${CRUD.todoTable}''';
final data = await db.rawQuery(sql);
List<ClassPenangkap> todos = List();
for (final node in data) {
final todo = ClassPenangkap.fromMap(node);
todos.add(todo);
}
return todos;
}
............

PENJELASAN

Variable nanti akan dimasukkan ke dalam syntax SQL untuk eksekusi insert, update, dan delete. Method pada insert,update, dan delete sturctur dan logicnya sama. Hanya syntax SQL nya yang berbeda. Jadi variable Database db akan membukakan akses ke table database. variable String sql akan berisi syntax SQL — nya. Variable List<Dynamic> params, hanya akan berisi nilai yang diberikan user dengan cara nanti sistem akan memasukkanya hasil input nya ke dalam kelas “penangkap”, dan setiap method akan menerimanya jika dipanggil. Setelah itu baru menjalankan fungsi asli method tersebut didalam variable result. Kenapa dalam tipe Integer? karena ketika sistem berhasil dieksekusi, nilai yang dikeluarkan adalah 1.

Setelah itu ada getContacList(). Bisa dilihat dari fungsi method ini dari syntax SQLnya. Setelah mendapatkan semua data dari sql, setiap data(per baris) akan di masukkan kembali ke kelas “Penangkap” dan di tambahkan ke dalam variable List<ClassPenangkap> todos menggunakan forEach looping, dan setelah selesai baru nilai-nya dikembalikan.

Tapi, kalian juga bisa menggunakan versi lainnya.

.......................
AccesDatabase dbHelper = new AccesDatabase();
Future<int> insert(ClassPenangkap todo) async {
Database db = await dbHelper.initDb();
int count = await db.insert('contact', todo.toMap());
return count;
}
Future<int> update(ClassPenangkap todo) async {
Database db = await dbHelper.initDb();
int count = await db.update('contact', todo.toMap(),
where: 'id=?', whereArgs: [todo.id]);
return count;
}
Future<int> delete(ClassPenangkap todo) async {
Database db = await dbHelper.initDb();
int count =
await db.delete('contact', where: 'id=?', whereArgs: [todo.id]);
return count;
}
Future<List<ClassPenangkap>> getContactList() async {
Database db = await dbHelper.initDb();
List<Map<String, dynamic>> mapList =
await db.query('contact', orderBy: 'name');
int count = mapList.length;
List<ClassPenangkap> contactList = List<ClassPenangkap>();
for (int i = 0; i < count; i++) {
contactList.add(ClassPenangkap.fromMap(mapList[i]));
}
return contactList;
}
............

Tidak jauh beda dengan yang di atas. Ini hanya versi singkat karena yang ini hanya menggunakan method dari plugin SQLite tersebut. Ini untuk kedua versi di atas.

Next, kita buat tampilan awalnya. Buat dulu file dan classnya bernama home dan gunakan statefull class.

Didalam _HomeState, kita akan mulai mengisinya dengan 2 variable dan 2 method.

  CRUD dbHelper = CRUD();
Future<List<ClassPenangkap>> future;
@override
void initState() {
super.initState();
updateListView();
}
void updateListView() {
setState(() {
future = dbHelper.getContactList();
});
}

PENJELASAN.

CRUD dbHelper, akan digunakan untuk memanggil method di dalamnya. Future<List<ClassPenangkap>> future, kita gunakan nanti untuk menyimpan seluruh data dalam databasenya di sini. Lalu ada initState(), dan ada updateListView yang berisikan setState().

Kemudia kita buat method untuk berpindah ke halaman yang akan berfungsi sebagai menambah atau mengedit data.

Future<ClassPenangkap> navigateToEntryForm(
BuildContext context, ClassPenangkap contact) async {
var result = await
Navigator.push(context,MaterialPageRoute(builder: (BuildContext context) {
return EntryForm(contact);
}));
return result;
}

Alasan kita masih menggunakan future akan saya jelaskan nanti. Dan, kita belum membuat EnteryForm .

Kemudia kita isi widget nya:

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Daftar Data-Data'),
),
body: FutureBuilder<List<ClassPenangkap>>(
future: future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: snapshot.data.map((todo) => cardo(todo)).toList());
} else {
return SizedBox();
}
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
tooltip: 'Tambah Data',
onPressed: () async {
var contact = await navigateToEntryForm(context, null);
if (contact != null) {
int result = await dbHelper.insert(contact);
if (result > 0) {
updateListView();
}
}
},
),
);
}

FutureBuilder

Widget yang dibangun sendiri berdasarkan snapshot terbaru dari interaksi dengan Future. Kita akan membuat Listnya dengan FutureBuilder.

PENJELASAN

Propderty future berfungsi untuk menangkap seluruh nilai didalam variable bertipe Future<List> untuk dapat mengakses data didalamnya dengan memindahkannya ke dalam varable snapshot. Fungsi properti future juga sebagai itemCount pada ListView.Builder .

Lalu untuk syntax ini:

if (snapshot.hasData) {
return Column(
children:snapshot.data.map((todo) =>cardo(todo)).toList());
} else {
return SizedBox();
}
},

FutureBuilder akan membuatkan list dari setiap widget yang akan dibuat dalam method map, dan diatur secara list. Tentunya sama seperti ListView.builder hanya dengan menggunakan 1 widget saja yang berisi data dari SQL. Nama widget itu adalah cardo ini syntaxnya:

Card cardo(ClassPenangkap contact) {
return Card(
color: Colors.white,
elevation: 2.0,
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.red,
child: Icon(Icons.people),
),
title: Text(
contact.name,
),
subtitle: Text(contact.phone.toString()),
trailing: GestureDetector(
child: Icon(Icons.delete),
onTap: () async {
int result = await dbHelper.delete(contact);
if (result > 0) {
updateListView();
}
},
),
onTap: () async {
var contact2 = await navigateToEntryForm(context, contact);
if (contact2 != null) {
int result = await dbHelper.update(contact2);
if (result > 0) {
updateListView();
}
}
},
),
);
}

Ini adalah widget yang akan kita panggil nanti. Yang bentukannya seperti ini nanti.

Jika kalian selalu mengikuti “Series” ini maka kalian sudah bisa menebak fungsi dari setiap syntax di atas untuk membuat TileList widget ini.

Lalu kita buat tampilan untuk menambah dan mengedit data-nya.

Kita gunakan Statefull dan juga kita beri constructor untuk menerima data dari class Home.

Kita membutuhkan 2 TextEditingController

TextEditingController nameController = TextEditingController();
TextEditingController phoneController = TextEditingController();

Didalam Widget builder kita beri if condition, seperti ini:

if (contact != null) {
nameController.text = contact.name;
phoneController.text = contact.phone;
}

Untuk memeriksa apakah user ingin menambahkan atau mengedit dari yang halaman utama(class home) kirim.

Selanjutnya kita akan membuat widget nya.

return Scaffold(
appBar: AppBar(
title: contact == null ? Text('Tambah') : Text('Rubah'),
leading: Icon(Icons.keyboard_arrow_left),
),
body: Padding(
padding: EdgeInsets.only(top: 15.0, left:10.0, right:10.0),
child: ListView(
children: <Widget> [
// nama
Padding (
padding: EdgeInsets.only(top:20.0, bottom:20.0),
child: TextField(
controller: nameController,
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: 'Nama Lengkap',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
),
),
Padding (
padding: EdgeInsets.only(top:20.0, bottom:20.0),
child: TextField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: 'Telepon',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5.0),
),
),
),
),
Padding (
padding: EdgeInsets.only(top:20.0, bottom:20.0),
child: Row(
children: <Widget> [
Expanded(
child: RaisedButton(
color: Theme.of(context).primaryColorDark,
textColor: Theme.of(context).primaryColorLight,
child: Text(
'Save',
textScaleFactor: 1.5,
),
onPressed: () {
if (contact == null) {
contact = ClassPenangkap(nameController.text, phoneController.text);
} else {
contact.name = nameController.text;
contact.phone = phoneController.text;
}
Navigator.pop(context, contact);
},
),
),
Container(width: 5.0,),
Expanded(
child: RaisedButton(
color: Theme.of(context).primaryColorDark,
textColor: Theme.of(context).primaryColorLight,
child: Text(
'Cancel',
textScaleFactor: 1.5,
),
onPressed: () {
Navigator.pop(context);
},
),
),
],
),
),
],
),
)
);

PENJELASAN

Pada appbar, title akan berubah sesuai situasi. Jika user tidak mengirimkan apa — apa, akan bertuliskan “Tambah” dan sebaliknya.

child: Text(
'Save',
textScaleFactor: 1.5,
),
onPressed: () {
if (contact == null) {
contact = ClassPenangkap(nameController.text, phoneController.text);
} else {
contact.name = nameController.text;
contact.phone = phoneController.text;
}
Navigator.pop(context, contact);
},

Ketika user berpindah dari halaman utama ke halaman edit/tambah ini, seperti yang dijelaskan di atas, situasi Constructor mempengaruhi fungsi halaman ini. Jika user menekan floating action button dari home class, artinya construtor di atur kosong dan halaman ini menjadi “tambah”.

FloatingActionButton, adalah button static yang selalu ada dibawah layar.

Dan jika user menekan TileList di home class, itu artinya consturtor diatur berisi dan halaman ini menjadi “edit”. onTap dari TileList jika ada yang lupa:

onTap: () async {
var contact2 = await navigateToEntryForm(context, contact);
if (contact2 != null) {
int result = await dbHelper.update(contact2);
if (result > 0) {
updateListView();
}
}
},

Setelah itu jika user menekan save, eksekusinya baru akan terjadi saat halaman kembali ke class home. Maka dari itu kita masih menggunakan Future di class home. Dengan syntax “Navigator.pop(context, contact);” di button save, aplikasi kembali dengan membawa nilai baru yang disimpan di kelas “penangkap” dan baru melakukan eksekusi apakah itu “edit” atau “adding” sesuai apa yang user tekan.

Dan yang terakhir, jangan lupa mengisi file main.dart, dengan syntax ini, untuk memunculkan halaman utama/class home.

import 'home.dart';
import 'package:flutter/material.dart';
//package letak folder Anda
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//judul
title: 'Tambahkan Daftar',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: Home(),
);
}
}

Dan, selamat aplikasi pertama menggunakan SQLite kalian jadi. Harusnya tampilan dari aplikasi kalian seperti ini.

Sekian dari saya untuk kali ini. Kurang lebih nya mohon maaf, sampai jumpa!!!!

--

--