Pemrograman Dart
Encapsulation, Library-Level Privacy (_), Getter, dan Setter
Pahami cara kerja encapsulation dan visibilitas privat (_) berbasis file/library di Dart 3, penggunaan getter, setter, computed property, serta anggota static.

Bayangkan kita kembali ke class RekeningBank yang kita buat sebelumnya. Meskipun kita sudah menyediakan method tarikTunai() yang memeriksa apakah saldo mencukupi, selama properti saldo bersifat publik, kode di luar class tetap bisa menulis rekeningBudi.saldo = -9999999; atau rekeningBudi.saldo = 1000000000; secara langsung tanpa melalui pemeriksaan apa pun! Pilar pertama Object-Oriented Programming, yaitu Encapsulation (Enkapsulasi), menyelesaikan masalah ini dengan menyembunyikan data internal objek dari akses luar secara sembarangan dan hanya menyediakan jalur akses resmi yang terkontrol melalui method, getter, dan setter.
Yang akan dipelajari
- Memahami perbedaan penting konsep visibilitas privat di Dart (
_) yang bekerja pada tingkat library/file, bukan tingkatclass. - Melindungi integritas data (state) objek menggunakan properti privat (
_namaField),get, danset. - Membuat properti turunan (computed property) menggunakan
gettanpa menyimpan variabel tambahan. - Menggunakan kata kunci
staticuntuk berbagi konstanta atau method milikclasstanpa perlu membuat objek.
Cara kerja library-level privacy (_) di Dart
Jika Anda pernah belajar Java, C++, atau C#, Anda mungkin terbiasa dengan kata kunci public, private, dan protected. Dart tidak memiliki ketiga kata kunci tersebut. Di Dart, aturan visibilitasnya sangat sederhana:
- Semua nama variabel, class, field, atau method yang tidak diawali garis bawah (
_) bersifat publik. - Semua nama yang diawali garis bawah (
_)—seperti_saldoatau_hitungBunga()—bersifat privat terhadap file (library) tempat ia didefinisikan.
Penting: Karena privasi
_di Dart berlaku pada level file (library), jika Anda menaruhclass RekeningBankdan fungsivoid main()di dalam satu file yang sama (misalnya di DartPad), fungsimain()akan tetap bisa mengakses_saldo! Inilah alasan utama mengapa sejak Bab 3 kita memisahkan definisi class di dalam folderlib/dan fungsimain()di dalam folderbin/.
| Lokasi Kode Pengakses | Mengakses namaPemilik (Publik) |
Mengakses _saldo (Privat _) |
|---|---|---|
Di dalam class RekeningBank (lib/rekening.dart) |
Bisa diakses | Bisa diakses |
Class atau fungsi lain di file yang sama (lib/rekening.dart) |
Bisa diakses | Bisa diakses (masih satu library) |
File lain yang mengimpor (bin/main.dart) |
Bisa diakses | Ditolak saat kompilasi (Error) |
Mari kita buktikan langsung dengan membuat file lib/dompet_digital.dart:
// File: lib/dompet_digital.dart
class DompetDigital {
final String pemilik;
// Field privat: diawali garis bawah (_), tidak bisa diubah langsung dari file lain
int _saldo = 0;
String _pin;
DompetDigital({
required this.pemilik,
required String pinAwal,
int saldoAwal = 0,
}) : _pin = pinAwal,
_saldo = saldoAwal >= 0 ? saldoAwal : 0;
// Getter: memberikan akses baca (read-only) terhadap _saldo ke file luar
int get saldo => _saldo;
// Method internal privat: hanya bisa dipanggil di dalam file ini
bool _verifikasiPin(String pinMasukan) => _pin == pinMasukan;
void topUp(int nominal) {
if (nominal < 10000) {
print('Gagal: Minimal top-up adalah Rp10.000.');
return;
}
_saldo += nominal;
print('Top-up Rp$nominal berhasil. Saldo $pemilik sekarang: Rp$_saldo');
}
bool bayar(int nominal, {required String pin}) {
if (!_verifikasiPin(pin)) {
print('Transaksi ditolak: PIN salah!');
return false;
}
if (nominal <= 0 || nominal > _saldo) {
print('Transaksi ditolak: Saldo tidak mencukupi atau nominal tidak valid.');
return false;
}
_saldo -= nominal;
print('Pembayaran Rp$nominal berhasil. Sisa saldo: Rp$_saldo');
return true;
}
}
Sekarang kita gunakan DompetDigital dari file bin/belajar_dart_oop.dart:
// File: bin/belajar_dart_oop.dart
import 'package:belajar_dart_oop/dompet_digital.dart';
void main() {
var dompetRina = DompetDigital(
pemilik: 'Rina Kartika',
pinAwal: '123456',
saldoAwal: 100000,
);
// Kita bisa MEMBACA saldo lewat getter 'dompetRina.saldo'
print('Saldo awal ${dompetRina.pemilik}: Rp${dompetRina.saldo}');
// Baris di bawah ini akan menghasilkan COMPILE ERROR jika tanda komentarnya dihapus:
// dompetRina._saldo = 99999999; // Error: The setter '_saldo' isn't defined
// dompetRina.saldo = 99999999; // Error: karena hanya ada getter, tidak ada setter
dompetRina.topUp(50000);
dompetRina.bayar(30000, pin: '000000'); // Ditolak karena PIN salah
dompetRina.bayar(30000, pin: '123456'); // Berhasil
}
Keluaran program saat dijalankan dengan dart run:
Saldo awal Rina Kartika: Rp100000
Top-up Rp50000 berhasil. Saldo Rina Kartika sekarang: Rp150000
Transaksi ditolak: PIN salah!
Pembayaran Rp30000 berhasil. Sisa saldo: Rp120000
Getter, Setter, dan computed property
Di Dart, getter (get) dan setter (set) terlihat seperti akses properti biasa dari sisi pemanggil (objek.properti), namun di balik layar keduanya menjalankan blok fungsi.
- Getter (
Tipe get nama => ...): Digunakan untuk mengekspos nilai field privat secara read-only atau menghitung nilai turunan (computed property) secara dinamis. - Setter (
set nama(Tipe nilaiBaru) { ... }): Digunakan ketika kita mengizinkan kode luar mengubah suatu properti, tetapi perubahan tersebut harus melewati aturan validasi terlebih dahulu.
Mari kita buat file lib/produk_diskon.dart yang mendemonstrasikan getter, setter bervalidasi, dan computed property:
// File: lib/produk_diskon.dart
class ProdukDiskon {
String nama;
int _hargaAsli;
double _persenDiskon = 0.0;
ProdukDiskon({
required this.nama,
required int hargaAsli,
}) : _hargaAsli = hargaAsli > 0 ? hargaAsli : 0;
// Getter & Setter untuk hargaAsli (menolak harga negatif atau nol)
int get hargaAsli => _hargaAsli;
set hargaAsli(int nilaiBaru) {
if (nilaiBaru <= 0) {
print('Peringatan: Harga harus lebih besar dari Rp0. Perubahan diabaikan.');
return;
}
_hargaAsli = nilaiBaru;
}
// Getter & Setter untuk persenDiskon (membatasi di rentang 0% - 90%)
double get persenDiskon => _persenDiskon;
set persenDiskon(double nilaiBaru) {
if (nilaiBaru < 0 || nilaiBaru > 90) {
print('Peringatan: Diskon harus berada di antara 0% hingga 90%.');
return;
}
_persenDiskon = nilaiBaru;
}
// Computed property: tidak memiliki field tersendiri, dihitung langsung saat dibaca
int get nominalPotongan => (_hargaAsli * (_persenDiskon / 100)).round();
int get hargaSetelahDiskon => _hargaAsli - nominalPotongan;
}
Ketika digunakan di bin/belajar_dart_oop.dart:
// File: bin/belajar_dart_oop.dart
import 'package:belajar_dart_oop/produk_diskon.dart';
void main() {
var sepatu = ProdukDiskon(nama: 'Sepatu Lari', hargaAsli: 400000);
// Memanggil setter sama seperti mengubah variabel biasa (menggunakan tanda =)
sepatu.persenDiskon = 25;
print('${sepatu.nama} | Harga Asli: Rp${sepatu.hargaAsli} '
'| Potongan: Rp${sepatu.nominalPotongan} '
'| Harga Akhir: Rp${sepatu.hargaSetelahDiskon}');
// Mencoba memasukkan nilai tidak valid ke setter
sepatu.persenDiskon = 120; // Ditolak oleh validasi di dalam setter
sepatu.hargaAsli = -50000; // Ditolak oleh validasi di dalam setter
print('Harga akhir tetap aman: Rp${sepatu.hargaSetelahDiskon}');
}
Keluaran program:
Sepatu Lari | Harga Asli: Rp400000 | Potongan: Rp100000 | Harga Akhir: Rp300000
Peringatan: Diskon harus berada di antara 0% hingga 90%.
Peringatan: Harga harus lebih besar dari Rp0. Perubahan diabaikan.
Harga akhir tetap aman: Rp300000
Anggota static: milik Class, bukan milik Object
Terkadang ada data atau fungsi pembantu yang berkaitan erat dengan suatu class, tetapi nilainya sama untuk seluruh objek (misalnya konstanta tarif pajak, penghitung jumlah objek yang telah dibuat, atau fungsi utilitas). Untuk kebutuhan ini, gunakan kata kunci static. Anggota static diakses langsung melalui nama class-nya (NamaClass.namaStatic), bukan melalui variabel objek:
class KonfigurasiAplikasi {
static const String versi = '3.2.0';
static int _jumlahSesiAktif = 0;
static int get jumlahSesiAktif => _jumlahSesiAktif;
static void mulaiSesiBaru() {
_jumlahSesiAktif++;
}
}
void main() {
// Diakses langsung menggunakan nama class tanpa membuat objek KonfigurasiAplikasi()
print('Versi Aplikasi: ${KonfigurasiAplikasi.versi}');
KonfigurasiAplikasi.mulaiSesiBaru();
KonfigurasiAplikasi.mulaiSesiBaru();
print('Sesi aktif saat ini: ${KonfigurasiAplikasi.jumlahSesiAktif}');
}
Keluaran program:
Versi Aplikasi: 3.2.0
Sesi aktif saat ini: 2
Kaitannya dengan Flutter / Dunia Nyata: Di Flutter, hampir setiap halaman yang memiliki data dinamis (
StatefulWidget) mempunyai pasangan class privat bernamaclass _HalamanUtamaState extends State<HalamanUtama>. Garis bawah pada_HalamanUtamaStatememastikan bahwa state internal halaman tersebut terenkapsulasi di dalam file halaman itu sendiri dan tidak bisa dimanipulasi secara tidak sengaja oleh file halaman lain.
Coba sendiri
Di dalam proyek VS Code Anda:
- Buka file
lib/dompet_digital.dartdan tambahkan satu methodvoid gantiPin({required String pinLama, required String pinBaru}). - Di dalam method tersebut, pastikan
pinLamacocok dengan_pindan panjangpinBaru.length == 6. Jika memenuhi syarat, perbarui_pin = pinBaru;. - Panggil
gantiPindaribin/belajar_dart_oop.dart, lalu coba lakukan pembayaran menggunakan PIN lama dan PIN baru melaluidart run.
Kesalahan yang sering terjadi
| Kesalahan Umum | Penyebab | Solusi yang Benar |
|---|---|---|
Bingung mengapa _saldo tetap bisa diubah langsung dari main() saat latihan di DartPad |
Menulis class dan main() di dalam satu file yang sama, padahal privasi _ di Dart berlaku per file (library), bukan per class. |
Pisahkan definisi class ke file tersendiri di dalam lib/ dan panggil dari file berbeda di bin/. |
Error Stack Overflow di dalam getter: int get saldo => saldo; |
Getter memanggil dirinya sendiri secara rekursif tanpa henti karena lupa menulis garis bawah _ pada field penyimpannya. |
Pastikan getter mengembalikan field privatnya: int get saldo => _saldo;. |
Menggunakan _namaField sebagai named parameter di constructor (Dompet({required this._pin})) |
Di Dart, named parameter publik tidak boleh diawali garis bawah _ karena garis bawah memiliki arti khusus (privat). |
Gunakan parameter bernama tanpa garis bawah, lalu isi di initializer list: Dompet({required String pin}) : _pin = pin;. |
| Membuat getter dan setter untuk semua field meskipun tidak ada logika validasi apa pun | Kebiasaan dari bahasa lain (seperti boilerplate getNama()/setNama() di Java). Di Dart, field publik biasa sudah otomatis memiliki implicit getter/setter. |
Mulai dari field publik biasa (String nama;); baru ubah menjadi _nama + get/set ketika Anda memang butuh membatasi akses atau menambah validasi. |
Latihan
Kerjakan tiga latihan berikut dengan memisahkan file model di lib/ dan pemanggilan main() di bin/:
- Enkapsulasi Termostat Ruangan: Buat
class Termostatdengan field privatdouble _suhuCelcius = 24.0. Sediakan gettersuhuCelcius, getter computed propertysuhuFahrenheit, serta settersuhuCelcius(double nilai)yang hanya menerima suhu di antara16.0hingga30.0derajat Celcius (jika di luar rentang, cetak pesan peringatan dan abaikan perubahan). - Penghitung Tiket Otomatis (
static& Privat): Buatclass TiketAntreanyang memiliki fieldstatic int _totalTiketKeluar = 0, serta field instancefinal int nomorUrutdanfinal String layanan. Setiap kali constructorTiketAntrean(this.layanan)dipanggil, naikkan_totalTiketKeluarsebesar 1 dan tetapkan nilainya kenomorUrut. Sediakanstatic int get totalTiketKeluardan getterString get kodeTiket => '$layanan-$nomorUrut'. - Akun Pengguna dengan Validasi Password: Buat
class AkunPenggunadenganfinal String usernamedan field privatString _password. Buat getterbool get passwordKuat => _password.length >= 8dan setterset passwordBaru(String nilai)yang menolak pembaruan jika panjang karakter kurang dari 8 atau mengandung spasi (nilai.contains(' ')).
Kunci jawaban dan pembahasan
Latihan 1
// File: lib/termostat.dart
class Termostat {
double _suhuCelcius = 24.0;
double get suhuCelcius => _suhuCelcius;
double get suhuFahrenheit => (_suhuCelcius * 9 / 5) + 32;
set suhuCelcius(double nilaiBaru) {
if (nilaiBaru < 16.0 || nilaiBaru > 30.0) {
print('Gagal: Suhu $nilaiBaru °C di luar batas aman (16.0 - 30.0 °C).');
return;
}
_suhuCelcius = nilaiBaru;
}
}
// File: bin/main.dart
import 'package:belajar_dart_oop/termostat.dart';
void main() {
var acKamar = Termostat();
acKamar.suhuCelcius = 20.0;
print('Suhu sekarang: ${acKamar.suhuCelcius} °C (${acKamar.suhuFahrenheit} °F)');
acKamar.suhuCelcius = 10.0; // Ditolak oleh setter
print('Suhu tetap : ${acKamar.suhuCelcius} °C');
}
Pembahasan: Dengan menyembunyikan _suhuCelcius di balik setter, objek Termostat dijamin tidak akan pernah berada pada suhu ekstrem di bawah 16 °C atau di atas 30 °C.
Latihan 2
// File: lib/tiket_antrean.dart
class TiketAntrean {
static int _totalTiketKeluar = 0;
final String layanan;
final int nomorUrut;
TiketAntrean(this.layanan) : nomorUrut = ++_totalTiketKeluar;
static int get totalTiketKeluar => _totalTiketKeluar;
String get kodeTiket => '$layanan-${nomorUrut.toString().padLeft(3, '0')}';
}
// File: bin/main.dart
import 'package:belajar_dart_oop/tiket_antrean.dart';
void main() {
var t1 = TiketAntrean('CS');
var t2 = TiketAntrean('TELLER');
var t3 = TiketAntrean('CS');
print(t1.kodeTiket); // CS-001
print(t2.kodeTiket); // TELLER-002
print(t3.kodeTiket); // CS-003
print('Total tiket tercetak hari ini: ${TiketAntrean.totalTiketKeluar}');
}
Pembahasan: Variabel static int _totalTiketKeluar dimiliki bersama oleh class TiketAntrean, sehingga setiap kali objek baru dibuat lewat constructor, nilai penghitung pusat bertambah satu (++_totalTiketKeluar) dan tidak ter-reset ke nol.
Latihan 3
// File: lib/akun_pengguna.dart
class AkunPengguna {
final String username;
String _password;
AkunPengguna({
required this.username,
required String passwordAwal,
}) : _password = passwordAwal;
bool get passwordKuat => _password.length >= 8 && !_password.contains(' ');
set passwordBaru(String nilai) {
if (nilai.length < 8) {
print('Gagal mengubah password: Minimal 8 karakter.');
return;
}
if (nilai.contains(' ')) {
print('Gagal mengubah password: Tidak boleh mengandung spasi.');
return;
}
_password = nilai;
print('Password untuk @$username berhasil diperbarui.');
}
}
// File: bin/main.dart
import 'package:belajar_dart_oop/akun_pengguna.dart';
void main() {
var akun = AkunPengguna(username: 'budi_dev', passwordAwal: 'rahasia123');
print('Apakah password awal kuat? ${akun.passwordKuat}');
akun.passwordBaru = '12345'; // Ditolak (kurang dari 8 karakter)
akun.passwordBaru = 'ada spasi 12'; // Ditolak (mengandung spasi)
akun.passwordBaru = 'Dart3Aman!'; // Berhasil
}
Pembahasan: Perhatikan bahwa kita sengaja tidak membuat getter untuk _password karena kata sandi asli tidak boleh dibaca kembali oleh kode luar; kita cukup menyediakan computed property bool get passwordKuat untuk memeriksa status keamanannya.
Diskusi (0)
Kami menyimpan nama (opsional), komentar, dan hash teknis (bukan alamat IP asli) untuk mencegah spam. Cookie fungsional dipakai agar Anda dapat mengedit atau menghapus komentar sendiri dari browser ini. Untuk permintaan penghapusan, gunakan halaman kontak.
Belum ada komentar. Mulai diskusinya.