Masukkan Password
59 Flutter Singleton class : How to create singleton in Dart
Video ini membahas Singleton Class di Dart, yaitu salah satu design pattern yang cukup penting ketika belajar OOP dan Flutter.
Saya akan jelaskan dari konsep paling dasar sampai tiga cara membuat Singleton yang dibahas di video. Saya juga akan meluruskan satu hal penting dari video: hashCode bukan bukti bahwa dua variabel menunjuk object yang sama. Untuk mengecek apakah dua reference benar-benar menunjuk instance yang sama, gunakan identical().
1. Apa itu Singleton?
Sebelum masuk ke kode, pahami dulu masalah yang ingin diselesaikan.
Misalnya kita punya class:
class Database {
Database() {
print("Database dibuat");
}
}
Kemudian:
void main() {
Database db1 = Database();
Database db2 = Database();
Database db3 = Database();
}
Maka kita membuat 3 object berbeda:
db1 → Database Object #1
db2 → Database Object #2
db3 → Database Object #3
Constructor akan dipanggil 3 kali.
Kalau Database ini sebenarnya merepresentasikan satu koneksi/database yang seharusnya hanya ada satu, membuat banyak object bisa menjadi tidak efisien.
Di sinilah Singleton digunakan.
2. Pengertian Singleton secara sederhana
Singleton adalah design pattern yang memastikan sebuah class hanya memiliki satu instance yang digunakan bersama selama lifetime aplikasi/program.
Gambaran sederhananya:
┌──────────────────┐
│ Singleton │
│ Object │
└──────────────────┘
↑ ↑ ↑
│ │ │
db1 db2 db3
Walaupun kita memiliki:
db1
db2
db3
ketiganya menunjuk ke object yang sama.
Jadi bukan:
db1 → Object A
db2 → Object B
db3 → Object C
tetapi:
db1 ─┐
db2 ─┼──→ Object Singleton
db3 ─┘
3. Kenapa Singleton diperlukan?
Biasanya Singleton digunakan ketika kita ingin memiliki satu titik akses terhadap resource atau service tertentu.
Contohnya:
- database manager
- configuration manager
- logging service
- authentication/session manager
- cache manager
- API/service manager
- application settings
Misalnya kita mempunyai ConfigManager.
Tidak masuk akal jika setiap bagian aplikasi membuat:
ConfigManager #1
ConfigManager #2
ConfigManager #3
ConfigManager #4
Padahal semuanya seharusnya membaca konfigurasi aplikasi yang sama.
Lebih masuk akal:
ConfigManager
↑
┌────────┼────────┐
│ │ │
Screen A Screen B Service
Semua menggunakan satu instance.
4. Apa yang ingin dicapai Singleton?
Ada dua tujuan utama:
1. Membatasi pembuatan object
Kita ingin mencegah kode seperti:
Database();
Database();
Database();
membuat banyak instance.
2. Memberikan akses ke instance yang sama
Misalnya:
Database db1 = Database();
Database db2 = Database();
kita ingin:
identical(db1, db2)
menghasilkan:
true
Artinya:
db1dandb2menunjuk ke instance yang sama.
5. Cara kerja Singleton
Secara konsep, Singleton membutuhkan tiga hal:
1. Constructor dibuat private
2. Simpan satu instance di static field
3. Berikan akses untuk mendapatkan instance tersebut
Mari kita bahas satu per satu.
6. Kenapa constructor dibuat private?
Dalam Dart, kita bisa membuat constructor private menggunakan _.
Contoh:
class Database {
Database._internal();
}
Perhatikan:
Database._internal();
Ada _ sebelum nama constructor.
Constructor tersebut private terhadap library tempat class itu didefinisikan.
Tujuannya agar kode dari luar library tidak bisa sembarangan melakukan:
Database._internal();
Dengan begitu kita bisa mengontrol bagaimana object Database dibuat.
7. Cara pertama: Singleton menggunakan Factory Constructor
Video pertama-tama menggunakan factory constructor.
Strukturnya:
class Database {
Database._internal();
static final Database _instance = Database._internal();
factory Database() {
return _instance;
}
}
Mari kita bedah.
7.1 Private constructor
Database._internal();
Ini adalah constructor yang digunakan untuk membuat instance sebenarnya.
Kita tidak ingin user langsung memanggilnya dari luar.
7.2 Static instance
Kemudian:
static final Database _instance = Database._internal();
Ini bagian yang sangat penting.
Kita membuat:
static
karena instance ini harus menjadi milik class, bukan milik masing-masing object.
Kemudian:
final
karena reference _instance tidak boleh diganti dengan object lain setelah diinisialisasi.
Jadi:
_instance
akan selalu menunjuk ke instance Singleton tersebut.
8. Kenapa static?
Bayangkan kita tidak menggunakan static:
class Database {
final Database _instance = Database._internal();
}
Masalahnya, setiap object Database akan mempunyai _instance sendiri.
Padahal kita ingin hanya ada satu instance untuk seluruh class.
Dengan:
static
kita mengatakan:
“Instance ini adalah milik class Database, bukan milik masing-masing object.”
Gambaran:
Database class
│
└── static _instance
│
↓
Database Object
9. Factory constructor
Kemudian:
factory Database() {
return _instance;
}
Artinya ketika seseorang menulis:
Database()
factory tidak membuat object baru.
Factory justru mengatakan:
"Ambil object yang sudah ada."
Jadi:
Database db1 = Database();
Database db2 = Database();
secara konsep:
db1 ──────┐
↓
_instance
↑
│
db2 ──────┘
10. Versi yang lebih pendek
Kode:
class Database {
Database._internal();
static final Database _instance = Database._internal();
factory Database() => _instance;
}
Ini adalah pola Singleton yang sangat umum di Dart.
Kemudian:
void main() {
final db1 = Database();
final db2 = Database();
print(identical(db1, db2));
}
Output:
true
Ini adalah pengecekan yang tepat untuk memastikan kedua reference menunjuk ke object yang sama.
11. Kenapa bukan hashCode?
Video mengatakan:
kalau hashCode sama berarti Singleton terbukti.
Ini kurang tepat.
Misalnya:
print(db1.hashCode);
print(db2.hashCode);
Kalau sama, itu memang bisa menjadi indikasi dalam kasus tertentu, tetapi hashCode bukan identitas object.
Dua object berbeda bisa saja memiliki hash code yang sama.
Jadi jangan menggunakan:
db1.hashCode == db2.hashCode
sebagai bukti pasti bahwa object sama.
Gunakan:
identical(db1, db2)
Contoh:
void main() {
final db1 = Database();
final db2 = Database();
print(identical(db1, db2));
}
Output:
true
12. Cara kedua: Singleton menggunakan static getter
Cara kedua yang dijelaskan video menggunakan getter.
Contoh:
class Database {
Database._internal();
static final Database _instance = Database._internal();
static Database get instance => _instance;
}
Sekarang cara mengambil Singleton berbeda.
Bukan:
Database()
tetapi:
Database.instance
Contoh:
void main() {
final db1 = Database.instance;
final db2 = Database.instance;
print(identical(db1, db2));
}
Output:
true
13. Apa itu getter?
Getter:
static Database get instance => _instance;
bisa dibaca seperti property.
Ketika kita menulis:
Database.instance
sebenarnya getter tersebut dijalankan dan mengembalikan:
_instance
Jadi:
Database.instance
↓
static getter
↓
_instance
↓
Singleton Object
14. Factory vs Getter
Sekarang bandingkan.
Factory
final db = Database();
dengan:
factory Database() => _instance;
Sedangkan getter:
final db = Database.instance;
dengan:
static Database get instance => _instance;
Keduanya bisa memberikan instance yang sama.
Perbedaannya terutama pada cara API-nya digunakan.
Factory membuat syntax terlihat seperti constructor:
Database()
Sedangkan getter secara eksplisit menunjukkan bahwa kita mengambil instance yang sudah tersedia:
Database.instance
15. Cara ketiga: langsung menggunakan static field
Cara ketiga yang dibahas video lebih sederhana lagi.
Contoh:
class Database {
Database._internal();
static final Database instance = Database._internal();
}
Kemudian:
void main() {
final db1 = Database.instance;
final db2 = Database.instance;
print(identical(db1, db2));
}
Output:
true
Ini sangat simpel.
Tidak membutuhkan:
factory
dan tidak membutuhkan:
getter
Kita langsung menyediakan instance melalui static field.
16. Tiga cara tersebut dibandingkan
Secara sederhana:
| Cara | Akses |
|---|---|
| Factory | Database() |
| Static getter | Database.instance |
| Static field | Database.instance |
Factory:
factory Database() => _instance;
Getter:
static Database get instance => _instance;
Static field:
static final Database instance = Database._internal();
Ketiganya dapat digunakan untuk membuat pola Singleton.
17. Kenapa final digunakan?
Perhatikan:
static final Database _instance = Database._internal();
Kenapa final?
Karena kita ingin reference _instance tidak diganti.
Misalnya:
static final Database _instance = Database._internal();
setelah dibuat, kita tidak ingin:
_instance = Database._internal();
menggantinya dengan object lain.
Tujuannya menjaga prinsip:
Satu class → satu instance yang digunakan sebagai Singleton.
18. Apakah final berarti object tidak bisa berubah?
Ini penting.
final Database db = Database();
final membuat reference/variabelnya tidak bisa diarahkan ke object lain.
Bukan berarti object-nya otomatis immutable.
Misalnya:
class Database {
Database._internal();
String status = "closed";
static final Database instance = Database._internal();
}
Kita masih bisa:
Database.instance.status = "open";
Jadi:
final
↓
reference tidak bisa diganti
bukan:
final
↓
seluruh isi object tidak bisa berubah
19. Gambaran lengkap Singleton
Mari gunakan versi factory:
class Database {
Database._internal();
static final Database _instance = Database._internal();
factory Database() => _instance;
}
Ketika program dimulai:
Database._internal()
↓
Object Database
↑
│
_instance
Kemudian:
final db1 = Database();
Factory mengembalikan:
_instance
Jadi:
db1 ──→ Database Object
Lalu:
final db2 = Database();
Factory lagi-lagi mengembalikan:
_instance
Jadi:
db1 ──┐
├──→ Database Object
db2 ──┘
20. Kenapa Singleton tidak bisa dibuat dengan constructor biasa?
Misalnya:
class Database {
Database();
}
Kemudian:
final db1 = Database();
final db2 = Database();
hasilnya:
db1 → Object A
db2 → Object B
Jadi kita perlu mekanisme yang mencegah constructor biasa membuat instance baru sembarangan.
Itulah mengapa constructor publik biasanya tidak disediakan.
21. Singleton dan private constructor
Bagian ini sangat penting untuk dipahami.
Kita punya:
Database._internal();
Jadi constructor yang benar-benar membuat object adalah:
Database._internal()
Kemudian object tersebut disimpan:
static final Database _instance = Database._internal();
Lalu pengguna mendapatkan object tersebut melalui:
factory Database() => _instance;
Maka alurnya:
Private Constructor
↓
Membuat 1 object
↓
static _instance
↓
factory/getter/static field
↓
Semua pengguna mendapatkan object yang sama
22. Contoh Singleton dengan state
Supaya konsepnya lebih terasa, kita buat contoh AppConfig.
class AppConfig {
AppConfig._internal();
static final AppConfig _instance = AppConfig._internal();
factory AppConfig() => _instance;
String environment = "development";
}
Kemudian:
void main() {
final config1 = AppConfig();
final config2 = AppConfig();
config1.environment = "production";
print(config2.environment);
}
Output:
production
Kenapa?
Karena:
config1
↓
└────┐
↓
AppConfig Object
↑
┌────┘
config2
Jadi ketika:
config1.environment = "production";
kita sebenarnya mengubah object yang juga digunakan oleh config2.
23. Singleton bukan berarti “tidak boleh ada banyak variabel”
Ini salah satu miskonsepsi yang sering terjadi.
Singleton bukan berarti hanya boleh ada satu variabel.
Kita masih boleh:
final a = AppConfig();
final b = AppConfig();
final c = AppConfig();
Yang penting:
a ──┐
b ──┼──→ satu object
c ──┘
Jadi yang dibatasi adalah:
jumlah instance/object
bukan jumlah reference/variabel.
24. Singleton bukan keyword Dart
Perlu diingat:
Tidak ada:
singleton class Database
Singleton bukan keyword seperti:
class
extends
implements
with
factory
Singleton adalah design pattern.
Artinya Singleton adalah pola desain yang kita bentuk menggunakan fitur-fitur Dart seperti:
- private constructor
staticfinalfactory- getter
25. Hubungannya dengan materi factory sebelumnya
Ini sangat berhubungan dengan materi sebelumnya tentang factory constructor.
Kita pernah membahas:
factory
tidak harus membuat object baru.
Factory boleh mengembalikan object yang sudah ada.
Singleton memanfaatkan kemampuan tersebut.
Contoh:
factory Database() => _instance;
Ketika:
Database()
dipanggil, factory tidak mengatakan:
“Buat Database baru.”
Tetapi:
“Kembalikan Database yang sudah disimpan di
_instance.”
Ini salah satu alasan factory constructor sangat cocok untuk Singleton.
26. Hubungannya dengan static
Singleton juga sangat erat dengan static.
Ingat:
static
berarti member tersebut berada pada level class, bukan masing-masing instance.
Karena Singleton membutuhkan satu instance yang dimiliki bersama oleh seluruh class, kita biasanya menyimpannya dalam:
static final
Contoh:
static final Database _instance = Database._internal();
Artinya kira-kira:
“Database class memiliki satu reference khusus yang menunjuk ke instance Singleton.”
27. Apakah Singleton selalu merupakan pilihan terbaik?
Tidak.
Singleton adalah design pattern, bukan aturan bahwa setiap service harus Singleton.
Singleton cocok ketika memang secara desain kita ingin:
“Satu instance bersama untuk seluruh aplikasi.”
Tetapi Singleton juga memiliki kekurangan.
Misalnya jika terlalu banyak hal dibuat Singleton, kode bisa menjadi terlalu bergantung pada global shared state.
Contohnya:
UserService
↓
Database
↓
Config
↓
Cache
↓
Logger
Kalau semuanya Singleton, dependency antar bagian aplikasi bisa menjadi sulit dilacak dan dites.
Jadi jangan berpikir:
“Singleton adalah cara terbaik membuat semua service.”
Lebih tepat:
“Singleton adalah salah satu solusi ketika memang hanya dibutuhkan satu shared instance.”
28. Singleton dalam Flutter
Dalam Flutter, konsep seperti ini bisa muncul pada service atau manager tertentu.
Misalnya:
class Logger {
Logger._internal();
static final Logger _instance = Logger._internal();
factory Logger() => _instance;
void log(String message) {
print(message);
}
}
Kemudian dari berbagai tempat:
Logger().log("User opened home page");
atau jika menggunakan static field:
Logger.instance.log("User opened home page");
Semua menggunakan instance Logger yang sama.
Namun dalam Flutter modern, untuk dependency management yang lebih kompleks, sering kali kita juga menggunakan pendekatan dependency injection/state-management daripada menjadikan semuanya Singleton secara manual.
29. Tiga implementasi lengkap
A. Factory Singleton
class Database {
Database._internal();
static final Database _instance = Database._internal();
factory Database() {
return _instance;
}
}
void main() {
final db1 = Database();
final db2 = Database();
print(identical(db1, db2));
}
Output:
true
B. Getter Singleton
class Database {
Database._internal();
static final Database _instance = Database._internal();
static Database get instance => _instance;
}
void main() {
final db1 = Database.instance;
final db2 = Database.instance;
print(identical(db1, db2));
}
Output:
true
C. Static Field Singleton
class Database {
Database._internal();
static final Database instance = Database._internal();
}
void main() {
final db1 = Database.instance;
final db2 = Database.instance;
print(identical(db1, db2));
}
Output:
true
30. Mana yang paling mudah?
Untuk pemula, saya sarankan memahami versi ini terlebih dahulu:
class Database {
Database._internal();
static final Database instance = Database._internal();
}
Cara menggunakannya:
final db1 = Database.instance;
final db2 = Database.instance;
Kemudian:
print(identical(db1, db2));
hasil:
true
Setelah memahami ini, baru pelajari versi factory:
class Database {
Database._internal();
static final Database _instance = Database._internal();
factory Database() => _instance;
}
31. Perbedaan class, interface, mixin, dan Singleton
Karena materi yang kamu pelajari sebelumnya saling berhubungan, coba lihat seperti ini:
| Konsep | Tujuan utama |
|---|---|
class | Membuat blueprint dan object |
abstract class | Blueprint yang tidak harus bisa dibuat object langsung |
implements | Memenuhi contract/interface |
mixin | Reuse behavior pada beberapa class |
| Singleton | Memastikan hanya ada satu instance |
factory | Mengontrol object yang dikembalikan constructor |
Jadi Singleton bukan pengganti class, interface, atau mixin.
Singleton adalah pola penggunaan sebuah class.
32. Hubungan dengan mixin yang baru kamu pelajari
Misalnya:
mixin LoggerMixin {
void log(String message) {
print(message);
}
}
Mixin digunakan untuk reuse behavior:
class UserService with LoggerMixin {}
class PaymentService with LoggerMixin {}
class ProductService with LoggerMixin {}
Sedangkan Singleton:
class Database {
Database._internal();
static final Database instance = Database._internal();
}
digunakan untuk membatasi instance menjadi satu.
Jadi:
Mixin
↓
"Bagaimana saya menggunakan behavior yang sama di beberapa class?"
Singleton
↓
"Bagaimana saya memastikan hanya ada satu instance?"
Ini dua masalah yang berbeda.
33. Hal paling penting yang harus kamu ingat
Kalau nanti ditanya:
Apa itu Singleton?
Jawaban sederhananya:
Singleton adalah design pattern yang memastikan sebuah class hanya memiliki satu instance yang dapat digunakan bersama oleh berbagai bagian program.
Kalau ditanya:
Bagaimana cara membuat Singleton di Dart?
Pola umumnya:
Private constructor
+
Static instance
+
Cara mengakses instance
Misalnya:
class Database {
Database._internal();
static final Database _instance = Database._internal();
factory Database() => _instance;
}
Dan untuk memastikan:
final a = Database();
final b = Database();
print(identical(a, b));
hasil:
true
34. Ringkasan super singkat
Bayangkan Singleton sebagai satu ruang kontrol.
Tanpa Singleton:
Program
├── Database A
├── Database B
├── Database C
└── Database D
Dengan Singleton:
Database
↑
┌─────────┼─────────┐
│ │ │
Service A Service B Service C
Semua memakai database instance yang sama.
Tiga cara dalam video:
1. Factory
factory Database() => _instance;
akses:
Database()
2. Getter
static Database get instance => _instance;
akses:
Database.instance
3. Static field
static final Database instance = Database._internal();
akses:
Database.instance
Dan satu koreksi penting dari video:
Jangan gunakan
hashCodeuntuk membuktikan dua object sama. Gunakanidentical(a, b).
Kalau kamu sudah memahami private constructor + static + final + factory, sebenarnya kamu sudah memahami inti dari Singleton di Dart.