Belajar Flutter dari Awal: Penyimpanan Data Lokal: Shared Preferences & SQLite (Part 9)
Rifqi An
Halo Flutter Devs kesayangan! Selamat datang kembali di seri Belajar Flutter dari Awal (Part 9)! Waduh, nggak kerasa ya udah nyampe part 9 aja. Kayak ngejar deadline padahal baru mulai ngoding. Di part-part sebelumnya, kita udah banyak banget bahas fundamental, mulai dari UI pake widget-widget ajaib Flutter, sampai interaksi sama API buat ngambil data dari internet. Keren banget kan?
Tapi, ada satu masalah klasik nih yang sering bikin kita garuk-garuk kepala, apalagi kalau lagi ngopi pas malam Jumat: "Gimana caranya data di aplikasi kita tetep ada, bahkan setelah aplikasi ditutup atau pas offline?" Nah, di sinilah keajaiban penyimpanan data lokal berperan. Ibaratnya, ini kayak punya kantong ajaib Doraemon di HP user, yang isinya data-data penting aplikasi kita.
Di artikel ini, kita bakal kupas tuntas dua jagoan utama untuk urusan simpan-menyimpan data lokal di Flutter: Shared Preferences dan SQLite. Siap-siap buat ngoding seru dan mungkin sedikit galau milih yang mana. Yuk, langsung gas!
Daftar Isi
- Kenapa Perlu Simpan Data Lokal?
- Shared Preferences: Si Penyimpan Data Ringan
- SQLite: Basis Data Relasional Mini
- Kapan Pakai Shared Preferences, Kapan Pakai SQLite?
- Penutup & Latihan Seru!
Kenapa Perlu Simpan Data Lokal?
Coba bayangin, kamu bikin aplikasi kalkulator keren. Setiap user buka, dia harus masukin lagi angka-angka yang kemarin dia hitung? Atau, aplikasi belanja online-mu, setiap dibuka, keranjang belanjanya kosong lagi? Kan nggak banget, ya. User pasti misuh-misuh (mengeluh) dan langsung uninstall.
Penyimpanan data lokal itu penting banget, cuy! Ini beberapa alasannya:
- Retensi Data: Data nggak hilang setelah aplikasi ditutup. Ini fundamental banget.
- Mode Offline: Aplikasi tetap bisa jalan (setidaknya sebagian) meskipun nggak ada koneksi internet. Penting nih buat yang suka ngoding di gunung tanpa sinyal.
- Pengaturan Pengguna: Nyimpen preferensi user kayak tema aplikasi (gelap/terang), ukuran font, notifikasi, dll. Biar user merasa aplikasi itu milik dia.
- Cache Data: Biar nggak bolak-balik minta data ke server, yang bisa bikin aplikasi jadi lemot dan boros kuota. Kayak nyimpen foto profil user, biar nggak download terus.
- Sesi Login: Setelah login, data user (token, ID) bisa disimpan lokal biar nggak perlu login ulang setiap buka aplikasi. Hemat waktu, hemat tenaga, hemat kuota!
Shared Preferences: Si Penyimpan Data Ringan
Apa itu Shared Preferences?
Oke, kita mulai dari yang paling gampang dan paling sering dipakai buat data-data "remeh-temeh" tapi penting: Shared Preferences. Kalau di dunia web, ini mirip banget sama localStorage atau sessionStorage. Dia nyimpen data dalam format key-value pair.
Bayangin kayak kamu punya kotak pos kecil. Setiap surat punya nama (key) dan isinya (value). Simpel kan? Shared Preferences ini paling cocok buat nyimpen data primitif (String, int, bool, double, atau List<String>) yang ukurannya kecil. Contohnya:
- Status login (
isLoggedIn: true) - Tema aplikasi (
appTheme: 'dark') - Pengaturan notifikasi (
enableNotifications: true) - Nama pengguna yang terakhir login (
lastLoggedInUser: 'budi')
Pokoknya, jangan coba-coba nyimpen seluruh daftar belanjaan yang panjang atau data user yang kompleks pake ini, nanti nangis di pojokan. Ada jagoan lain buat itu.
Cara Pakai Shared Preferences
Pertama dan utama, kita butuh package-nya. Buka file pubspec.yaml kamu dan tambahin ini:
dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.2.0 # Versi bisa disesuaikan dengan yang terbaru
Jangan lupa flutter pub get di terminal setelah nambahin itu, biar package-nya di-download. Sekarang, mari kita ngoding!
1. Inisialisasi dan Menyimpan Data
Untuk menyimpan data, kita perlu panggil SharedPreferences.getInstance() yang mengembalikan Future. Jadi, selalu pakai await ya!
import 'package:shared_preferences/shared_preferences.dart';
// Fungsi untuk menyimpan data
Future<void> saveData() async {
final prefs = await SharedPreferences.getInstance();
// Menyimpan String
await prefs.setString('username', 'FlutterHero');
// Menyimpan Integer
await prefs.setInt('userAge', 25);
// Menyimpan Boolean
await prefs.setBool('isLoggedIn', true);
// Menyimpan Double
await prefs.setDouble('appVersion', 1.0);
// Menyimpan List<String>
await prefs.setStringList('favFrameworks', ['Flutter', 'React Native', 'Ionic']);
print('Data berhasil disimpan!');
}
2. Membaca Data
Membaca data juga gampang, tinggal panggil metode get... sesuai tipe datanya. Kalau key-nya nggak ada, dia bakal balikin null. Jadi, penting buat ngecek atau kasih nilai default.
import 'package:shared_preferences/shared_preferences.dart';
// Fungsi untuk membaca data
Future<void> readData() async {
final prefs = await SharedPreferences.getInstance();
String? username = prefs.getString('username');
int? userAge = prefs.getInt('userAge');
bool? isLoggedIn = prefs.getBool('isLoggedIn');
double? appVersion = prefs.getDouble('appVersion');
List<String>? favFrameworks = prefs.getStringList('favFrameworks');
print('Username: ${username ?? 'N/A'}'); // Pakai ?? untuk default value
print('Age: ${userAge ?? 0}');
print('Logged In: ${isLoggedIn ?? false}');
print('App Version: ${appVersion ?? 0.0}');
print('Favorite Frameworks: ${favFrameworks ?? []}');
}
3. Menghapus Data
Mau bersih-bersih? Tinggal panggil remove() buat satu key tertentu, atau clear() buat ngehapus semua data dari aplikasi. Hati-hati pake clear() ya, jangan sampai data penting user ikut terhapus!
import 'package:shared_preferences/shared_preferences.dart';
// Fungsi untuk menghapus data
Future<void> deleteData() async {
final prefs = await SharedPreferences.getInstance();
// Menghapus data dengan key 'username'
await prefs.remove('username');
print('Username berhasil dihapus!');
// Menghapus semua data (gunakan dengan hati-hati!)
// await prefs.clear();
// print('Semua data berhasil dihapus!');
}
Gimana? Gampang banget kan pake Shared Preferences? Ini kayak ngambil permen dari toples, cepet dan nggak pake ribet. Tapi inget, cuma buat permen kecil ya, bukan buat kue ulang tahun!
SQLite: Basis Data Relasional Mini
Apa itu SQLite?
Kalau Shared Preferences itu kantong celana yang isinya recehan, maka SQLite itu dompet tebal yang isinya kartu-kartu penting, KTP, STNK, dan lain-lain yang lebih terstruktur. SQLite adalah sebuah sistem manajemen basis data relasional (RDBMS) yang sifatnya embedded, artinya dia bisa langsung ditanam di dalam aplikasi kita tanpa perlu server terpisah. Kerennya, dia mandiri banget!
SQLite cocok banget buat data yang:
- Terstruktur dan Kompleks: Punya banyak kolom, relasi antar tabel (misal: satu user punya banyak postingan, satu produk punya banyak review).
- Jumlahnya Banyak: Ratusan, ribuan, bahkan jutaan baris data bisa diatasi dengan baik.
- Perlu Query Canggih: Kamu butuh filter data, pengurutan, penggabungan data antar tabel, dll. Pakai SQL query kayak di MySQL atau PostgreSQL.
Contoh penggunaan: daftar tugas (to-do list), katalog produk, riwayat chat, data pengguna aplikasi yang kompleks, atau cache data dari API yang perlu diolah lebih lanjut.
Cara Pakai SQLite di Flutter dengan sqflite
Untuk berinteraksi dengan SQLite di Flutter, kita pakai package sqflite. Selain itu, kita juga butuh path_provider untuk menentukan lokasi penyimpanan database di perangkat. Tambahin di pubspec.yaml:
dependencies:
flutter:
sdk: flutter
sqflite: ^2.3.0+2 # Versi terbaru
path_provider: ^2.1.1 # Versi terbaru
Jangan lupa flutter pub get lagi!
Konsep Dasar: Model Data dan Database Helper
Untuk bekerja dengan SQLite secara terstruktur, biasanya kita bikin:
- Model Data (POJO/PODO): Sebuah kelas Dart yang merepresentasikan struktur tabel di database kita.
- Database Helper: Sebuah kelas yang bertugas mengelola koneksi database, membuat tabel, serta menyediakan method untuk operasi CRUD (Create, Read, Update, Delete).
Misalnya, kita mau bikin aplikasi "Catatan Harian". Kita butuh tabel untuk menyimpan catatan.
1. Membuat Model Data (Note.dart)
class Note {
int? id; // id akan auto-increment
String title;
String content;
DateTime date;
Note({this.id, required this.title, required this.content, required this.date});
// Convert Note object to Map (untuk disimpan ke DB)
Map<String, dynamic> toMap() {
return {
'id': id,
'title': title,
'content': content,
'date': date.toIso8601String(), // Simpan DateTime sebagai String
};
}
// Convert Map to Note object (untuk dibaca dari DB)
factory Note.fromMap(Map<String, dynamic> map) {
return Note(
id: map['id'],
title: map['title'],
content: map['content'],
date: DateTime.parse(map['date']),
);
}
}
2. Membuat Database Helper (DatabaseHelper.dart)
Ini bagian yang agak panjang, tapi intinya dia ngurusin koneksi, bikin tabel, dan operasi CRUD.
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; // Untuk mendapatkan direktori dokumen
import 'dart:io';
import 'note.dart'; // Import model Note kita
class DatabaseHelper {
static final _databaseName = "MyNotesDB.db"; // Nama file database kita
static final _databaseVersion = 1; // Versi database
static final table = 'notes'; // Nama tabel kita
static final columnId = 'id';
static final columnTitle = 'title';
static final columnContent = 'content';
static final columnDate = 'date';
// Make this a singleton class
DatabaseHelper._privateConstructor();
static final DatabaseHelper instance = DatabaseHelper._privateConstructor();
// Only have a single app-wide reference to the database
static Database? _database;
Future<Database> get database async {
if (_database != null) return _database!;
// Lazily instantiate the db the first time it is accessed
_database = await _initDatabase();
return _database!;
}
// This opens the database (and creates it if it doesn't exist)
_initDatabase() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, _databaseName);
return await openDatabase(path,
version: _databaseVersion,
onCreate: _onCreate);
}
// SQL code to create the database table
Future _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $table (
$columnId INTEGER PRIMARY KEY AUTOINCREMENT,
$columnTitle TEXT NOT NULL,
$columnContent TEXT NOT NULL,
$columnDate TEXT NOT NULL
)
''');
}
// --- CRUD operations ---
// Insert a note
Future<int> insert(Note note) async {
Database db = await instance.database;
return await db.insert(table, note.toMap());
}
// Get all notes
Future<List<Note>> getNotes() async {
Database db = await instance.database;
List<Map<String, dynamic>> maps = await db.query(table, orderBy: '$columnDate DESC'); // Urutkan berdasarkan tanggal terbaru
return List.generate(maps.length, (i) {
return Note.fromMap(maps[i]);
});
}
// Update a note
Future<int> update(Note note) async {
Database db = await instance.database;
int id = note.id!;
return await db.update(table, note.toMap(), where: '$columnId = ?', whereArgs: [id]);
}
// Delete a note
Future<int> delete(int id) async {
Database db = await instance.database;
return await db.delete(table, where: '$columnId = ?', whereArgs: [id]);
}
// Close the database (optional, Flutter handles it well usually)
Future<void> close() async {
_database?.close();
_database = null;
}
}
3. Menggunakan Database Helper di UI atau Logic
Sekarang, gimana caranya kita pakai DatabaseHelper ini di aplikasi kita? Gampang banget, tinggal panggil method-method yang udah kita bikin.
import 'package:flutter/material.dart';
import 'database_helper.dart'; // Import DatabaseHelper
import 'note.dart'; // Import Note model
class MyNotesScreen extends StatefulWidget {
@override
_MyNotesScreenState createState() => _MyNotesScreenState();
}
class _MyNotesScreenState extends State<MyNotesScreen> {
List<Note> _notes = [];
final DatabaseHelper _dbHelper = DatabaseHelper.instance;
@override
void initState() {
super.initState();
_refreshNotes();
}
Future<void> _refreshNotes() async {
final data = await _dbHelper.getNotes();
setState(() {
_notes = data;
});
}
Future<void> _addNote(String title, String content) async {
Note newNote = Note(
title: title,
content: content,
date: DateTime.now(),
);
await _dbHelper.insert(newNote);
_refreshNotes(); // Refresh list setelah nambah data
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Catatan baru berhasil ditambahkan!'))
);
}
Future<void> _updateNote(Note note) async {
await _dbHelper.update(note);
_refreshNotes(); // Refresh list setelah update
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Catatan berhasil diperbarui!'))
);
}
Future<void> _deleteNote(int id) async {
await _dbHelper.delete(id);
_refreshNotes(); // Refresh list setelah delete
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Catatan berhasil dihapus!'))
);
}
// Contoh UI untuk menampilkan dan berinteraksi
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('My Awesome Notes')),
body: _notes.isEmpty
? Center(child: Text('Belum ada catatan. Tambahkan satu!'))
: ListView.builder(
itemCount: _notes.length,
itemBuilder: (context, index) {
final note = _notes[index];
return Card(
margin: EdgeInsets.all(8.0),
child: ListTile(
title: Text(note.title, style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(
'${note.content}\n${note.date.toLocal().toString().split(' ')[0]}',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.edit, color: Colors.blue),
onPressed: () {
// Contoh: edit note
_updateNote(Note(
id: note.id,
title: '${note.title} (Edited)',
content: '${note.content} - ini updatean.',
date: DateTime.now()
));
},
),
IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => _deleteNote(note.id!),
),
],
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addNote(
'Catatan Baru ${DateTime.now().second}',
'Ini adalah isi catatan yang baru saja ditambahkan. Asik kan ngodingnya?'
),
child: Icon(Icons.add),
),
);
}
}
Waduh, kodenya lumayan panjang ya? Tapi jangan panik! Itu normal banget kalau udah mainan database. Intinya, kita bikin blueprint data (Note), bikin tukang kunci database (DatabaseHelper), terus baru deh pake di UI. Ini emang lebih kompleks dari Shared Preferences, tapi sepadan dengan kemampuannya buat ngelola data yang lebih terstruktur dan banyak.
Kapan Pakai Shared Preferences, Kapan Pakai SQLite?
Nah, ini pertanyaan sejuta umat! Jangan sampai salah pilih, nanti aplikasi jadi boros resource atau malah nggak optimal. Ini panduan recehnya:
-
Pakai Shared Preferences Kalau:
- Data yang disimpan sederhana (
String,int,bool,double,List<String>). - Jumlah data sedikit, paling cuma belasan atau puluhan key-value pair.
- Data tidak membutuhkan relasi antar tabel.
- Contoh: Tema aplikasi, status notifikasi, token autentikasi, pengaturan bahasa, nama user terakhir.
- Kapan butuh cepet dan nggak mau pusing sama struktur data.
- Data yang disimpan sederhana (
-
Pakai SQLite Kalau:
- Data yang disimpan kompleks, punya banyak atribut, dan membutuhkan struktur tabel.
- Jumlah data bisa sangat banyak (ratusan, ribuan, jutaan).
- Membutuhkan relasi antar data (misal: user punya banyak post, post punya banyak komentar).
- Perlu operasi query yang canggih (filter, sorting, join, agregasi).
- Contoh: Daftar produk, riwayat transaksi, cache data dari API, daftar kontak, catatan pribadi.
- Kapan kamu siap "lembur" sedikit demi performa dan struktur data yang rapi.
Pilih sesuai kebutuhan ya, jangan sampai cuma gara-gara mau nyimpen dark mode preference malah bikin database SQLite, itu namanya over-engineering, bikin pusing sendiri! 😂
Penutup & Latihan Seru!
Kesimpulan
Waduh, perjalanan kita kali ini cukup panjang dan penuh ilmu baru ya! Kita sudah belajar gimana cara menyimpan data lokal di aplikasi Flutter kesayangan kita. Mulai dari Shared Preferences yang super simpel buat data-data kecil dan receh, sampai SQLite yang powerful buat data yang terstruktur, kompleks, dan banyak.
Masing-masing punya kelebihan dan kekurangannya sendiri. Jadi, kuncinya adalah "pilih alat yang tepat untuk pekerjaan yang tepat". Jangan pernah takut buat eksplorasi dan mencoba hal baru, karena itulah esensi dari ngoding, kan?
Latihan Ngoding Bareng Aku!
Biar ilmu yang udah kita serap nggak cuma numpang lewat di otak, yuk kita praktik! Aku punya ide lucu buat latihan kita kali ini. Kita bikin aplikasi "Jurnal Curhat Programmer"!
Skenarionya: Kamu adalah seorang solo programmer yang sering lembur, ngopi, dan kadang frustasi sama bug. Kamu butuh tempat buat curhatin semua keluh kesah itu di aplikasi pribadimu.
-
Pake Shared Preferences:
- Buat pengaturan tema aplikasi (misal:
dark modeataulight mode) yang bisa diganti user. - Simpan nama panggilan user (misal: "Si Paling Ngoding") yang akan muncul sebagai greeting di
AppBar. - Simpan status "mood" terakhir user saat keluar aplikasi (misal: "Stres", "Senang", "Galau").
- Buat pengaturan tema aplikasi (misal:
-
Pake SQLite:
- Buat tabel untuk menyimpan curhatan. Setiap curhatan harus punya:
id(Primary Key, Auto-increment)judulcurhatan (misal: "Bug API Hari Ini", "Flutter Crash Lagi")isicurhatan (teks panjang)tanggalcurhattingkatStres(nilai integer dari 1 sampai 5, 1=santai, 5=mau banting keyboard)
- Implementasikan operasi CRUD:
- Menambahkan curhatan baru.
- Menampilkan semua daftar curhatan (urutkan dari yang terbaru).
- Mengedit curhatan yang sudah ada.
- Menghapus curhatan.
- Tampilkan daftar curhatan di
ListView.
- Buat tabel untuk menyimpan curhatan. Setiap curhatan harus punya:
Pokoknya, bikin tampilan sesimpel mungkin tapi fungsional. Anggap aja ini proyek iseng-iseng berhadiah (hadiahnya ilmu!). Kalau udah jadi, jangan lupa pamerin ke teman-teman ngodingmu ya! Siapa tahu jadi ide startup "Jejak Digital Stress Programmer". 😂
Selamat ngoding, para pahlawan keyboard! Sampai jumpa di part selanjutnya, semoga nggak ada bug di antara kita!
.png)