Persistensi lokal

Menyimpan data sederhana dengan shared_preferences dan mengenali sqflite untuk data relasional.

Daftar isi

Data yang hanya tersimpan di memori hilang begitu aplikasi ditutup, sehingga aplikasi perlu menyimpan sebagian datanya pada penyimpanan perangkat, dari tema yang dipilih hingga isi catatan. Topik ini membahas dua tingkat persistensi lokal di Flutter, yaitu shared_preferences untuk pasangan kunci-nilai sederhana dan sqflite untuk data relasional di atas SQLite. Batas masing-masing dibahas bersama kodenya, sehingga pemilihan tempat penyimpanan dapat dilakukan dengan alasan yang jelas.

shared_preferences: penyimpanan kunci-nilai#

shared_preferences adalah package resmi Flutter untuk menyimpan data dalam bentuk pasangan kunci-nilai (key-value), yaitu setiap nilai diakses melalui satu nama kunci yang tetap. Di balik layar, package ini membungkus penyimpanan bawaan platform, seperti SharedPreferences pada Android dan UserDefaults pada iOS, sehingga API-nya sama untuk semua platform. Nilai yang didukung berupa tipe primitif, dan metodenya dinamai menurut tipenya, yaitu setString, setInt, setBool, setDouble, serta setStringList untuk daftar string.

Aksesnya selalu asinkron. Objek SharedPreferences diperoleh melalui SharedPreferences.getInstance() yang mengembalikan Future, sehingga pemanggilannya ditandai await. Pembacaan menggunakan getter dengan pola nama yang sama, seperti getString dan getStringList, dan getter untuk kunci yang belum pernah ditulis mengembalikan null.

dart Contoh: menyimpan dan membaca pengaturan
// tidak-dijalankan
import 'package:shared_preferences/shared_preferences.dart';

Future<void> simpanPengaturan() async {
  final preferensi = await SharedPreferences.getInstance();
  await preferensi.setString('nama', 'Budi');
  await preferensi.setString('tema', 'gelap');
  await preferensi.setStringList('tugas', ['Rapat pukul 10', 'Kirim laporan']);
}

Future<void> bacaPengaturan() async {
  final preferensi = await SharedPreferences.getInstance();
  final nama = preferensi.getString('nama');
  final tema = preferensi.getString('tema');
  final tugas = preferensi.getStringList('tugas') ?? const <String>[];
  print('nama: $nama, tema: $tema, jumlah tugas: ${tugas.length}');
}

Polanya berlaku untuk pengaturan aplikasi, seperti tema, nama pengguna, atau penanda bahwa layar pengenalan sudah pernah dilewati. Operator ?? pada pembacaan daftar tugas memberi daftar kosong apabila kuncinya belum ada, sehingga kode berikutnya tidak perlu menangani null. Pola ini berlaku untuk semua getter, sehingga nilai bawaan dapat ditentukan langsung pada tempat pembacaannya.

Konsep kunci-nilai dalam program yang dapat dijalankan#

Perilaku kunci-nilai dapat dibuktikan tanpa perangkat dan tanpa Flutter. Program berikut meniru penyimpanannya dengan Map, lengkap dengan dua aturan pokoknya, yaitu nilai tersimpan dalam bentuk string dan kunci yang belum ditulis bernilai null. Daftar tugas diserialisasi dengan jsonEncode dari pembahasan JSON agar sesuai aturan pertama. Simpan sebagai kunci-nilai.dart, lalu jalankan dengan perintah dart kunci-nilai.dart.

dart kunci-nilai.dart
// Menyimulasi penyimpanan kunci-nilai ala shared_preferences dengan Dart murni.
import 'dart:convert';

void main() {
  // Map berperan sebagai penyimpanan pasangan kunci-nilai.
  final preferensi = <String, String>{};

  // Menulis pasangan kunci-nilai, seperti setString.
  preferensi['nama'] = 'Budi';
  preferensi['tema'] = 'gelap';

  // List disimpan sebagai satu string, serupa setStringList yang
  // menyimpan List<String> di balik layar.
  final tugas = ['Rapat pukul 10', 'Kirim laporan', 'Beli buku'];
  preferensi['tugas'] = jsonEncode(tugas);

  // Membaca kembali lewat kunci, seperti getString.
  print('nama: ${preferensi['nama']}');
  print('tema: ${preferensi['tema']}');

  // String JSON diurai kembali menjadi List<String>.
  final daftarTugas = (jsonDecode(preferensi['tugas']!) as List).cast<String>();
  print('jumlah tugas: ${daftarTugas.length}');
  print('tugas pertama: ${daftarTugas[0]}');

  // Kunci yang belum pernah ditulis bernilai null.
  print('kunci bahasa: ${preferensi['bahasa']}');
  print('jumlah kunci tersimpan: ${preferensi.length}');
}
text Keluaran
nama: Budi
tema: gelap
jumlah tugas: 3
tugas pertama: Rapat pukul 10
kunci bahasa: null
jumlah kunci tersimpan: 3

Baris kunci bahasa: null pada keluaran memperlihatkan perilaku yang sama dengan getter pada shared_preferences, yaitu kunci yang belum ditulis tidak melempar error, melainkan bernilai null. Nilai kunci tugas yang awalnya berupa List kembali sebagai List<String> setelah diurai lagi, sehingga data yang disimpan dan dibaca tetap simetris.

Simulasi ini menggunakan Map karena perbedaannya dengan penyimpanan sesungguhnya hanya ada pada tempat data berada, bukan pada cara mengaksesnya. Pada shared_preferences, isi Map itu digantikan penyimpanan milik aplikasi yang bertahan setelah aplikasi ditutup, sedangkan bentuk aksesnya tetap melalui kunci. Karena itu, program di atas dapat dibaca sebagai kontrak perilaku yang sama dengan versi aslinya pada perangkat.

Batas shared_preferences#

Batas tersebut membuat perannya jelas. Penyimpanan ini bukan basis data, sehingga tidak ada pencarian, pengurutan, maupun penghitungan yang dikerjakan oleh penyimpanannya sendiri. Operasi semacam itu berarti mengambil seluruh nilai lalu memprosesnya di Dart, dan biayanya tumbuh seiring isi yang tersimpan. Nilainya juga terbatas pada tipe primitif, sehingga objek yang berlapis harus diserialisasi terlebih dahulu dan tidak dapat diperbarui sebagian.

Batas ini bukan kekurangan yang perlu dihindari, melainkan kesepakatan desain. Untuk puluhan nilai kecil yang ditulis dan dibaca utuh, pendekatan kunci-nilai adalah pilihan paling murah dan paling mudah dibaca. Persoalan baru muncul ketika data mulai berbentuk kumpulan baris yang panjangnya tidak terkendali, dan kebutuhan semacam itu adalah wilayah kerja basis data.

sqflite: SQLite tertanam untuk data relasional#

SQLite adalah mesin basis data relasional tertanam yang paling luas digunakan di dunia, dan sqflite adalah package yang membawanya ke Flutter. Basis data yang dibuatnya berupa satu berkas di penyimpanan aplikasi, isinya tersusun dalam tabel dengan kolom yang bertipe, dan operasinya dikerjakan dengan SQL. Data yang berulang, seperti catatan dengan judul, isi, dan tanggalnya, disimpan sebagai baris, sehingga ratusan ribu baris dapat dicari dan diurutkan tanpa memuat semuanya ke memori.

Kode berikut memperlihatkan bentuk umumnya, yaitu membuka basis data, membuat tabel saat pertama kali dibuka, menambah baris, dan mencari baris dengan kondisi.

dart Contoh: tabel catatan dengan query berparameter
// tidak-dijalankan
import 'package:sqflite/sqflite.dart';

late final Database db;

Future<void> bukaBasisData() async {
  db = await openDatabase('catatan.db', version: 1, onCreate: (db, versi) async {
    await db.execute('''
      CREATE TABLE catatan(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        judul TEXT NOT NULL,
        isi TEXT NOT NULL
      )
    ''');
  });
}

Future<void> tambahCatatan(String judul, String isi) async {
  await db.insert('catatan', {'judul': judul, 'isi': isi});
}

Future<List<Map<String, Object?>>> cariCatatan(String kata) async {
  return db.query(
    'catatan',
    where: 'judul LIKE ?',
    whereArgs: ['%$kata%'],
  );
}

openDatabase membuka berkas catatan.db dan mengeksekusi CREATE TABLE hanya pada onCreate, yaitu saat berkasnya pertama kali dibuat. Metode db.insert menerima Map yang kuncinya nama kolom, sedangkan db.query menerima kondisi where dengan tanda ? yang nilainya diberikan melalui whereArgs. Bentuk berparameter ini menjaga masukan pengguna tetap sebagai data, bukan bagian teks SQL, sehingga kesalahan tanda kutip dan injeksi SQL terhindar.

Kapan berpindah ke sqflite#

KebutuhanPilihan pantas
tema, nama pengguna, penanda layar pengenalanshared_preferences
daftar tugas pendek yang dibaca dan ditulis utuhshared_preferences
ratusan catatan yang dapat dicari berdasarkan judulnyasqflite
data yang perlu diurutkan, difilter, atau dihitungsqflite
relasi antarjenis data, misalnya catatan dan tagnyasqflite

Aturan umumnya mengikuti bentuk data. Selama data masih berupa beberapa nilai lepas yang dibaca dan ditulis utuh, shared_preferences sudah memadai dan kodenya paling pendek. Kebutuhan berpindah ke sqflite ketika data berbentuk kumpulan baris yang panjangnya terus bertambah, ketika pencarian dan pengurutan dibutuhkan, atau ketika dua jenis data saling terkait. Keduanya tidak saling menggantikan secara otomatis, karena data yang sudah tersimpan di salah satu tempat harus dipindahkan sendiri apabila pilihan berubah.

Kesalahan umum#

  1. Melupakan await pada SharedPreferences.getInstance(). Tanpa await, variabel berisi Future, sehingga pemanggilan setString pada variabel tersebut ditolak penganalisis Dart sebagai metode yang tidak ada. Perbaikannya, tandai await sebelum objeknya digunakan.
  2. Menyimpan objek sebagai string tersusun sendiri. Nilai yang digabung dengan pemisah buatan sendiri tidak dapat diurai kembali dengan andal begitu isinya mengandung pemisah itu. Perbaikannya, serialisasi dengan jsonEncode dan baca kembali dengan jsonDecode, atau pindah ke sqflite.
  3. Menyisipkan masukan pengguna langsung ke teks SQL. Interpolasi string pada where membuka celah injeksi SQL dan memicu error sintaks pada tanda kutip. Perbaikannya, gunakan tanda ? beserta whereArgs.
  4. Membuka basis data pada setiap query. Memanggil openDatabase dari setiap fungsi menambah panggilan asinkron yang tidak perlu dan memperlambat pembukaan layar. Perbaikannya, buka satu kali, lalu simpan objek Database-nya untuk digunakan bersama.

Ringkasan#

  • shared_preferences menyimpan pasangan kunci-nilai primitif secara asinkron melalui SharedPreferences.getInstance().
  • Kunci yang belum ditulis bernilai null, sehingga pembacaan dilengkapi nilai bawaan lewat ??.
  • sqflite menyediakan SQLite tertanam berupa tabel dan query SQL berparameter untuk data relasional.
  • Pemilihannya mengikuti bentuk data, yaitu nilai kecil pada kunci-nilai dan kumpulan baris yang diolah dengan query SQL.

Referensi#