Belajar Flutter dari Awal: Penyimpanan Data Lokal: Shared Preferences & SQLite (Part 9)

Rifqi An Rifqi An
Maret 02, 2026

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?

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:

  1. Model Data (POJO/PODO): Sebuah kelas Dart yang merepresentasikan struktur tabel di database kita.
  2. 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.
  • 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.

  1. Pake Shared Preferences:
    • Buat pengaturan tema aplikasi (misal: dark mode atau light 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").
  2. Pake SQLite:
    • Buat tabel untuk menyimpan curhatan. Setiap curhatan harus punya:
      • id (Primary Key, Auto-increment)
      • judul curhatan (misal: "Bug API Hari Ini", "Flutter Crash Lagi")
      • isi curhatan (teks panjang)
      • tanggal curhat
      • tingkatStres (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.

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!

Bagikan Artikel Ini