Pemrograman Dart
Abstraction: abstract class, implements, dan Class Modifiers Dart 3
Kuasai pilar keempat OOP di Dart 3: merancang kontrak dengan abstract class, memahami perbedaan extends vs implements, serta menggunakan interface, base, final, dan sealed class.

Saat merancang aplikasi berskala besar, sering kali kita punya konsep umum yang terlalu abstrak untuk dibuat objeknya secara langsung. Misalnya, kita tahu setiap BangunDatar pasti memiliki hitungLuas(), tetapi kita tidak pernah menggambar "BangunDatar" tanpa bentuk yang jelas—yang kita gambar selalu Persegi, Lingkaran, atau Segitiga. Begitu pula saat bekerja dalam tim: kita ingin menetapkan kontrak (daftar method yang wajib ada) tanpa memedulikan bagaimana setiap anggota tim mengimplementasikan detail di balik layarnya. Pilar keempat OOP ini disebut Abstraction (abstraksi), dan Dart 3 melengkapinya dengan fitur Class Modifiers yang sangat kuat.
Yang akan dipelajari
- Membuat
abstract classdan abstract method untuk memisahkan kontrak dari detail implementasi. - Memahami konsep implicit interface di Dart serta perbedaan mendasar antara
extendsdanimplements. - Mengimplementasikan lebih dari satu interface sekaligus pada satu kelas.
- Mengenal class modifiers di Dart 3:
interface class,base class,final class, danabstract interface class. - Memanfaatkan
sealed classbersama exhaustiveswitchuntuk pemodelan state yang bebas celah.
Konsep abstract class dan Abstract Method
Kelas abstrak (abstract class) adalah kelas yang tidak dapat diinstansiasi secara langsung menjadi objek. Kelas ini berfungsi sebagai cetak biru dasar atau kontrak bagi kelas-kelas turunannya.
Di dalam abstract class, kita bisa mendeklarasikan दो jenis method:
- Abstract method: method yang hanya memiliki tanda tangan (nama, parameter, dan tipe kembalian) diakhiri titik koma
;tanpa badan{ ... }. Setiap subclass konkret (non-abstrak) wajib meng-override method ini. - Concrete method: method biasa yang sudah memiliki isi kode
{ ... }dan langsung diwariskan ke subclass.
abstract class BangunDatar {
final String namaBentuk;
BangunDatar(this.namaBentuk);
// Abstract method: tidak punya body, wajib di-override oleh subclass
double hitungLuas();
double hitungKeliling();
// Concrete method: punya body, boleh langsung dipakai oleh subclass
void cetakInfo() {
print(
'$namaBentuk -> Luas: ${hitungLuas().toStringAsFixed(2)}, '
'Keliling: ${hitungKeliling().toStringAsFixed(2)}',
);
}
}
class PersegiPanjang extends BangunDatar {
final double panjang;
final double lebar;
PersegiPanjang({required this.panjang, required this.lebar})
: super('Persegi Panjang');
@override
double hitungLuas() => panjang * lebar;
@override
double hitungKeliling() => 2 * (panjang + lebar);
}
class Lingkaran extends BangunDatar {
final double jariJari;
Lingkaran({required this.jariJari}) : super('Lingkaran');
@override
double hitungLuas() => 3.14159 * jariJari * jariJari;
@override
double hitungKeliling() => 2 * 3.14159 * jariJari;
}
void main() {
// final bentuk = BangunDatar('Umum'); // ERROR: Abstract classes can't be instantiated.
final daftarBangun = <BangunDatar>[
PersegiPanjang(panjang: 8, lebar: 5),
Lingkaran(jariJari: 7),
];
for (final bangun in daftarBangun) {
bangun.cetakInfo();
}
}
Output:
Persegi Panjang -> Luas: 40.00, Keliling: 26.00
Lingkaran -> Luas: 153.94, Keliling: 43.98
Implicit Interface: extends vs implements
Di bahasa pemrograman seperti Java atau C#, terdapat kata kunci khusus interface. Di Dart, setiap class secara implisit juga mendefinisikan sebuah interface yang berisi seluruh deklarasi field (getter/setter) dan method publik dari kelas tersebut.
Ketika kita menghubungkan dua kelas, kita bisa memilih antara extends (mewarisi implementasi) atau implements (mematuhi kontrak interface):
| Aspek Perbandingan | class A extends B (Pewarisan) |
class A implements B (Kontrak Interface) |
|---|---|---|
| Hubungan | A adalah subtipe yang mewarisi isi B | A berjanji menyediakan seluruh perilaku publik B |
| Jumlah kelas target | Hanya 1 kelas (single inheritance) | Bisa banyak kelas (implements B, C, D) |
| Mewarisi badan method & field induk? | Ya, otomatis diwarisi | Tidak, semua method dan field publik dari B wajib ditulis ulang (@override) di A |
Wajib memanggil constructor induk (super)? |
Ya | Tidak, constructor tidak ikut dalam kontrak implements |
Mari kita lihat bagaimana sebuah kelas SmartTV dapat mengimplementasikan lebih dari satu kontrak sekaligus:
abstract class PerangkatAudio {
void aturVolume(int level);
}
abstract class PerangkatJaringan {
bool sambungkanWifi(String ssid);
}
class SmartTV implements PerangkatAudio, PerangkatJaringan {
final String merek;
int _volumeSaatIni = 10;
SmartTV(this.merek);
@override
void aturVolume(int level) {
_volumeSaatIni = level.clamp(0, 100);
print('[$merek] Volume diatur ke $_volumeSaatIni%');
}
@override
bool sambungkanWifi(String ssid) {
print('[$merek] Tersambung ke jaringan Wi-Fi "$ssid"');
return true;
}
}
void main() {
final tvRuangTamu = SmartTV('NusantaraVision 4K');
tvRuangTamu.sambungkanWifi('Rumah_5G');
tvRuangTamu.aturVolume(35);
}
Output:
[NusantaraVision 4K] Tersambung ke jaringan Wi-Fi "Rumah_5G"
[NusantaraVision 4K] Volume diatur ke 35%
Class Modifiers di Dart 3
Sejak Dart 3, kita mendapatkan Class Modifiers (pengubah kelas) untuk mengontrol dengan tegas bagaimana sebuah kelas boleh digunakan di luar file/library tempatnya didefinisikan. Berikut ringkasan lengkapnya:
| Deklarasi Kelas | Bisa Diinstansiasi? | Bisa Di-extends? |
Bisa Di-implements? |
Kegunaan Utama |
|---|---|---|---|---|
class (biasa) |
Ya | Ya | Ya | Kelas umum tanpa batasan |
abstract class |
Tidak | Ya | Ya | Kelas dasar yang boleh punya implementasi parsial |
interface class |
Ya | Tidak (di luar library) | Ya | Kelas yang di luar library hanya boleh dijadikan kontrak |
abstract interface class |
Tidak | Tidak (di luar library) | Ya | Murni interface (paling direkomendasikan untuk kontrak repository/service) |
base class |
Ya | Ya | Tidak (di luar library) | Memastikan subclass selalu mewarisi implementasi asli dan private field induk |
final class |
Ya | Tidak (di luar library) | Tidak (di luar library) | Menutup hirarki pewarisan sepenuhnya |
sealed class |
Tidak | Hanya di library yang sama | Hanya di library yang sama | Membuat kumpulan subtipe tertutup yang bisa diperiksa tuntas (exhaustive) oleh switch |
Catatan: Aturan pembatasan
extendsdanimplementspadainterface,base, danfinalberlaku ketika kelas tersebut diakses dari file/library lain. Untuk kontrak murni sepertiAuthRepositoryatauDatabaseService, praktik terbaik di Dart 3 adalah menuliskannya sebagaiabstract interface class.
Contoh abstract interface class untuk Kontrak Penyimpanan
// Kontrak murni: tidak bisa diinstansiasi dan tidak bisa di-extends di luar library
abstract interface class GudangDataPelanggan {
void simpanPelanggan(String id, String nama);
String? cariPelanggan(String id);
}
class GudangMemoriLokal implements GudangDataPelanggan {
final Map<String, String> _penyimpanan = {};
@override
void simpanPelanggan(String id, String nama) {
_penyimpanan[id] = nama;
print('Data [$id: $nama] tersimpan di RAM.');
}
@override
String? cariPelanggan(String id) => _penyimpanan[id];
}
void main() {
// Variabel bertipe interface, objeknya implementasi konkret
final GudangDataPelanggan gudang = GudangMemoriLokal();
gudang.simpanPelanggan('PLG-01', 'Nadia Putri');
print('Hasil pencarian PLG-01: ${gudang.cariPelanggan('PLG-01')}');
}
Output:
Data [PLG-01: Nadia Putri] tersimpan di RAM.
Hasil pencarian PLG-01: Nadia Putri
sealed class dan Exhaustive switch di Dart 3
Salah satu fitur paling disukai di Dart 3 adalah sealed class. Kelas berlabel sealed secara otomatis bersifat abstract (tidak bisa diinstansiasi langsung) dan seluruh subclass-nya wajib berada di dalam file yang sama.
Karena compiler Dart mengetahui 100% daftar seluruh subclass dari sebuah sealed class, kita bisa menggunakan switch yang exhaustive (tuntas). Jika kita lupa menangani salah satu subclass di dalam switch, compiler akan langsung menolak kode kita sebelum program dijalankan!
sealed class StatusPembayaran {}
class MenungguPembayaran extends StatusPembayaran {
final String kodeVirtualAccount;
MenungguPembayaran(this.kodeVirtualAccount);
}
class PembayaranBerhasil extends StatusPembayaran {
final String nomorReferensi;
final int nominal;
PembayaranBerhasil({required this.nomorReferensi, required this.nominal});
}
class PembayaranGagal extends StatusPembayaran {
final String alasan;
PembayaranGagal(this.alasan);
}
String buatPesanLayar(StatusPembayaran status) {
// Switch expression Dart 3 memeriksa seluruh subtipe StatusPembayaran
return switch (status) {
MenungguPembayaran(kodeVirtualAccount: final va) =>
'Silakan transfer ke nomor VA: $va',
PembayaranBerhasil(nomorReferensi: final ref, nominal: final jml) =>
'Lunas Rp$jml (Ref: $ref). Terima kasih!',
PembayaranGagal(alasan: final sebab) =>
'Transaksi gagal: $sebab. Silakan coba lagi.',
};
}
void main() {
final daftarStatus = <StatusPembayaran>[
MenungguPembayaran('8801992011'),
PembayaranBerhasil(nomorReferensi: 'TRX-991', nominal: 150000),
PembayaranGagal('Saldo tidak mencukupi'),
];
for (final s in daftarStatus) {
print(buatPesanLayar(s));
}
}
Output:
Silakan transfer ke nomor VA: 8801992011
Lunas Rp150000 (Ref: TRX-991). Terima kasih!
Transaksi gagal: Saldo tidak mencukupi. Silakan coba lagi.
Coba sendiri
Buka proyek latihan Anda di VS Code dan buat file bin/uji_sealed.dart:
- Salin kode
StatusPembayarandi atas dan jalankandart run bin/uji_sealed.dart. - Tambahkan satu kelas baru di bawah
PembayaranGagal:class PembayaranKadaluarsa extends StatusPembayaran {} - Tanpa mengubah fungsi
buatPesanLayar, perhatikan garis merah di VS Code atau jalankandart run bin/uji_sealed.dart. Dart akan memberikan pesan error jelas: The type 'StatusPembayaran' is not exhaustively matched by the switch cases since it doesn't match 'PembayaranKadaluarsa()'. - Tambahkan cabang
PembayaranKadaluarsa() => 'Waktu pembayaran telah habis.',di dalamswitchhingga program kembali berjalan mulus.
Kesalahan yang sering terjadi
| Kesalahan Pemula | Mengapa Bermasalah? | Solusi yang Benar |
|---|---|---|
Mencoba membuat objek langsung dari abstract class atau sealed class |
Kelas abstrak belum memiliki implementasi yang lengkap untuk diinstansiasi | Buat objek dari subclass konkretnya, misalnya final BangunDatar b = Lingkaran(jariJari: 5); |
Menggunakan implements padahal ingin mewarisi kode dan field yang sudah jadi di induk |
Saat memakai implements, seluruh field dan method induk harus ditulis ulang satu per satu |
Gunakan extends jika ingin mewarisi kode/perilaku, dan gunakan implements hanya jika ingin mematuhi kontrak antarmuka |
Menaruh subclass dari sealed class di file .dart yang berbeda |
Melanggar aturan sealed karena compiler harus mengetahui semua subtipe dalam satu library yang sama |
Letakkan sealed class beserta seluruh subclass langsungnya di dalam satu file .dart yang sama |
Menambahkan cabang default: atau _ => secara sembarangan pada switch untuk sealed class |
Menghilangkan manfaat compile-time check ketika ada subtipe baru yang ditambahkan di kemudian hari | Tulis setiap subclass secara eksplisit di dalam switch agar compiler selalu mengingatkan jika ada kasus yang terlewat |
Kaitannya dengan Flutter / Dunia Nyata: Arsitektur aplikasi Flutter modern (seperti Clean Architecture) sangat mengandalkan
abstract interface classdansealed class. Pertama, lapisan logika aplikasi tidak pernah memanggil API atau database secara langsung, melainkan bergantung pada kontrak sepertiabstract interface class ProdukRepository. Saat pengujian otomatis (unit testing), kita cukup membuatclass FakeProdukRepository implements ProdukRepositorytanpa perlu koneksi internet! Kedua,sealed classadalah standar emas untuk merepresentasikan State layar di Flutter (misalnyaLoadingState,LoadedState,ErrorState), sehingga kita tidak akan pernah lupa menampilkan indikator loading atau pesan gagal di layar pengguna.
Latihan
- Kontrak Ekspor Dokumen (
abstract interface class): Buat sebuahabstract interface class DapatDiekspordengan methodString eksporKeTeks(). Buat dua kelas yang mengimplementasikannya:FakturPenjualan(propertinomorFakturdantotal) sertaLaporanAbsensi(propertibulandanjumlahHadir). Masukkan kedua objek ke dalamList<DapatDiekspor>dan cetak hasileksporKeTeks()-nya. - Abstraksi Pengiriman (
abstract class): Buatabstract class KurirEkspedisidengan propertinamaKurir, abstract methodint estimasiHari(double jarakKm), dan concrete methodvoid tampilkanEstimasi(double jarakKm). Buat dua subclass menggunakanextends:KurirReguler(1 hari per 100 km, dibulatkan ke atas dengan.ceil()) danKurirInstan(selalu 1 hari jika jarak <= 40 km, selebihnya 2 hari). - Pemodelan Hasil Login (
sealed class): Buatsealed class HasilLoginbeserta tiga kelas turunannya:LoginSukses(menyimpannamaUserdanperan),LoginSalahPassword(menyimpansisaPercobaan), danAkunTerkunci(menyimpandurasiMenit). Buat fungsiString tanganiLogin(HasilLogin hasil)yang menggunakan switch expression untuk menghasilkan pesan yang sesuai bagi ketiga kondisi tersebut.
Kunci jawaban dan pembahasan
Latihan 1
abstract interface class DapatDiekspor {
String eksporKeTeks();
}
class FakturPenjualan implements DapatDiekspor {
final String nomorFaktur;
final int total;
FakturPenjualan({required this.nomorFaktur, required this.total});
@override
String eksporKeTeks() => 'FAKTUR|$nomorFaktur|Rp$total';
}
class LaporanAbsensi implements DapatDiekspor {
final String bulan;
final int jumlahHadir;
LaporanAbsensi({required this.bulan, required this.jumlahHadir});
@override
String eksporKeTeks() => 'ABSENSI|$bulan|Hadir: $jumlahHadir hari';
}
void main() {
final antreanEkspor = <DapatDiekspor>[
FakturPenjualan(nomorFaktur: 'INV-2026-01', total: 450000),
LaporanAbsensi(bulan: 'Oktober', jumlahHadir: 22),
];
for (final dokumen in antreanEkspor) {
print(dokumen.eksporKeTeks());
}
}
Pembahasan: FakturPenjualan dan LaporanAbsensi sama sekali tidak memiliki hubungan garis keturunan (is-a), namun keduanya sama-sama mematuhi kontrak DapatDiekspor melalui implements. Dengan abstract interface class, kita memastikan tidak ada kode di luar library yang salah kaprah melakukan extends terhadap kontrak ini.
Latihan 2
abstract class KurirEkspedisi {
final String namaKurir;
KurirEkspedisi(this.namaKurir);
int estimasiHari(double jarakKm);
void tampilkanEstimasi(double jarakKm) {
final hari = estimasiHari(jarakKm);
print('[$namaKurir] Jarak ${jarakKm}km -> Estimasi tiba: $hari hari');
}
}
class KurirReguler extends KurirEkspedisi {
KurirReguler() : super('Reguler Nusantara');
@override
int estimasiHari(double jarakKm) => (jarakKm / 100).ceil();
}
class KurirInstan extends KurirEkspedisi {
KurirInstan() : super('Kilat Express');
@override
int estimasiHari(double jarakKm) => jarakKm <= 40 ? 1 : 2;
}
void main() {
final reguler = KurirReguler();
final instan = KurirInstan();
reguler.tampilkanEstimasi(230);
instan.tampilkanEstimasi(25);
}
Pembahasan: KurirEkspedisi menggabungkan method konkret tampilkanEstimasi() (agar format cetak seragam dan tidak ditulis berulang) dengan method abstrak estimasiHari() (yang rumusnya diserahkan kepada masing-masing subclass).
Latihan 3
sealed class HasilLogin {}
class LoginSukses extends HasilLogin {
final String namaUser;
final String peran;
LoginSukses({required this.namaUser, required this.peran});
}
class LoginSalahPassword extends HasilLogin {
final int sisaPercobaan;
LoginSalahPassword(this.sisaPercobaan);
}
class AkunTerkunci extends HasilLogin {
final int durasiMenit;
AkunTerkunci(this.durasiMenit);
}
String tanganiLogin(HasilLogin hasil) {
return switch (hasil) {
LoginSukses(namaUser: final user, peran: final role) =>
'Selamat datang, $user! Anda masuk sebagai $role.',
LoginSalahPassword(sisaPercobaan: final sisa) =>
'Kata sandi salah. Sisa kesempatan mencoba: $sisa kali.',
AkunTerkunci(durasiMenit: final menit) =>
'Akun Anda dikunci sementara. Coba lagi setelah $menit menit.',
};
}
void main() {
print(tanganiLogin(LoginSalahPassword(2)));
print(tanganiLogin(AkunTerkunci(15)));
print(tanganiLogin(LoginSukses(namaUser: 'Farhan', peran: 'Admin')));
}
Pembahasan: Dengan sealed class HasilLogin, fungsi tanganiLogin dijamin menangani seluruh kemungkinan keluarnya proses autentikasi. Pattern matching di dalam switch sekaligus mengekstrak properti dari masing-masing subclass dengan ringkas dan type-safe.
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.