Pemrograman Dart
Komposisi Perilaku: mixin, Enhanced enum, dan extension
Pelajari cara membagikan perilaku lintas kelas tanpa pewarisan kaku menggunakan mixin, membangun enum pintar dengan properti dan method, serta memperluas tipe bawaan melalui extension.

Pewarisan (extends) sangat ampuh ketika dua kelas memiliki hubungan garis keturunan yang jelas (is-a). Akan tetapi, di dunia nyata kita sering menjumpai kemampuan atau perilaku yang dimiliki oleh berbagai kelas dari hirarki yang sama sekali berbeda. Misalnya, baik kelas TransaksiPenjualan maupun PerubahanStokGudang sama-sama butuh kemampuan "mencatat log waktu audit", padahal keduanya bukan keturunan satu sama lain. Karena Dart hanya mengizinkan satu extends (single inheritance), memaksakan kelas induk bersama akan membuat desain menjadi kaku. Untuk mengatasi hal ini—serta membuat kode lebih ekspresif—Dart menyediakan tiga fitur andalan: mixin, Enhanced enum, dan extension.
Yang akan dipelajari
- Membagikan kode dan kemampuan lintas hirarki kelas menggunakan
mixindan kata kunciwith. - Membatasi kelas mana saja yang boleh memakai sebuah mixin dengan klausa
on. - Merancang Enhanced Enum di Dart yang memiliki properti
final, constructorconst, dan method. - Menambahkan method atau getter baru ke tipe data yang sudah ada (seperti
intatauString) menggunakanextension.
Membagikan Perilaku dengan mixin dan with
mixin adalah cara untuk menggunakan kembali (reuse) sekumpulan properti dan method di berbagai kelas tanpa harus membentuk hubungan induk-anak (extends). Jika extends menyatakan hubungan "adalah sebuah" (is-a), maka mixin menyatakan kemampuan "dapat melakukan" (can-do / has-capability).
Untuk membuat mixin, gunakan kata kunci mixin NamaMixin { ... }. Kemudian, tempelkan mixin tersebut ke kelas mana pun menggunakan kata kunci with (kita bahkan bisa menempelkan beberapa mixin sekaligus dipisahkan koma):
| Mekanisme | Kata Kunci | Mewarisi Kode Siap Pakai? | Jumlah yang Boleh Dipakai | Tujuan Utama |
|---|---|---|---|---|
| Pewarisan | extends |
Ya | Maksimal 1 kelas | Hirarki utama (is-a) |
| Kontrak | implements |
Tidak (wajib tulis ulang semua) | Banyak interface | Mematuhi kontrak API |
| Komposisi Perilaku | with (mixin) |
Ya | Banyak mixin | Menempelkan fitur/kemampuan lintas kelas |
Berikut contoh bagaimana mixin PencatatAudit dan mixin DapatDivalidasi ditempelkan ke kelas yang berbeda:
mixin PencatatAudit {
final List<String> _riwayatLog = [];
void catatAktivitas(String pesan) {
final waktu = DateTime.now().toIso8601String().substring(11, 19);
_riwayatLog.add('[$waktu] $pesan');
}
void cetakLogAudit() {
print('--- Riwayat Audit (${_riwayatLog.length} entri) ---');
for (final log in _riwayatLog) {
print(log);
}
}
}
mixin DapatDivalidasi {
bool apakahNominalValid(int nominal) => nominal > 0;
}
class DompetDigital with PencatatAudit, DapatDivalidasi {
final String pemilik;
int _saldo = 0;
DompetDigital(this.pemilik);
void isiSaldo(int nominal) {
if (!apakahNominalValid(nominal)) {
catatAktivitas('GAGAL top-up nominal tidak valid: Rp$nominal');
return;
}
_saldo += nominal;
catatAktivitas('Top-up berhasil Rp$nominal. Saldo kini: Rp$_saldo');
}
}
void main() {
final dompet = DompetDigital('Tari');
dompet.isiSaldo(150000);
dompet.isiSaldo(-5000);
dompet.isiSaldo(50000);
dompet.cetakLogAudit();
}
Output:
--- Riwayat Audit (3 entri) ---
[09:30:12] Top-up berhasil Rp150000. Saldo kini: Rp150000
[09:30:12] GAGAL top-up nominal tidak valid: Rp-5000
[09:30:12] Top-up berhasil Rp50000. Saldo kini: Rp200000
Membatasi Penggunaan Mixin dengan Klausa on
Terkadang sebuah mixin perlu memanggil method atau mengakses properti yang hanya ada pada kelas induk tertentu. Kita dapat menambahkan kata kunci on NamaKelasInduk pada deklarasi mixin. Dengan begitu, mixin tersebut hanya boleh dipakai oleh kelas yang merupakan turunan dari NamaKelasInduk:
abstract class Karyawan {
final String nama;
final int gajiPokok;
Karyawan({required this.nama, required this.gajiPokok});
}
// Mixin ini HANYA bisa ditempelkan pada subclass dari Karyawan
mixin HitungLembur on Karyawan {
int hitungUpahLembur(int jamLembur) {
// Bisa langsung mengakses properti `nama` dan `gajiPokok` milik Karyawan!
final tarifPerJam = gajiPokok ~/ 173;
final totalLembur = tarifPerJam * jamLembur;
print('Lembur $nama ($jamLembur jam): Rp$totalLembur');
return totalLembur;
}
}
class TeknisiLapangan extends Karyawan with HitungLembur {
TeknisiLapangan({required super.nama, required super.gajiPokok});
}
void main() {
final teknisi = TeknisiLapangan(nama: 'Bagas', gajiPokok: 6920000);
final bonusLembur = teknisi.hitungUpahLembur(5);
print('Total dibawa pulang: Rp${teknisi.gajiPokok + bonusLembur}');
}
Output:
Lembur Bagas (5 jam): Rp200000
Total dibawa pulang: Rp7120000
Enhanced enum: Enumerasi Pintar di Dart
Pada bahasa pemrograman lama, enum hanyalah daftar nama konstanta sederhana (seperti enum Hari { senin, selasa, ... }). Sejak Dart 2.17 dan disempurnakan di Dart 3, kita memiliki Enhanced Enum: sebuah enum dapat memiliki field yang bersifat final, memiliki const constructor, bahkan memiliki getter dan method layaknya sebuah kelas!
Syarat membuat Enhanced Enum:
- Semua field instans harus dideklarasikan
final. - Constructor-nya wajib bersifat
const. - Daftar nilai konstanta ditulis paling atas, dipisahkan koma
,, dan diakhiri tanda titik koma;sebelum menuliskan field atau method.
enum TingkatanMember {
reguler(label: 'Member Reguler', diskonPersen: 0, minimalPoin: 0),
perak(label: 'Member Perak', diskonPersen: 5, minimalPoin: 100),
emas(label: 'Member Emas', diskonPersen: 12, minimalPoin: 500),
platinum(label: 'Member Platinum', diskonPersen: 20, minimalPoin: 1500); // Akhiri daftar dengan titik koma (;)
final String label;
final int diskonPersen;
final int minimalPoin;
const TingkatanMember({
required this.label,
required this.diskonPersen,
required this.minimalPoin,
});
double hitungHargaAkhir(double hargaAwal) {
final potongan = hargaAwal * (diskonPersen / 100);
return hargaAwal - potongan;
}
bool apakahMemenuhiSyarat(int poinPelanggan) => poinPelanggan >= minimalPoin;
}
void main() {
const memberSaatIni = TingkatanMember.emas;
print('Status: ${memberSaatIni.label} (Diskon ${memberSaatIni.diskonPersen}%)');
print('Belanja Rp250.000 menjadi: Rp${memberSaatIni.hitungHargaAkhir(250000).toStringAsFixed(0)}');
// Menelusuri seluruh nilai enum menggunakan .values
final poinUser = 620;
for (final tingkat in TingkatanMember.values) {
if (tingkat.apakahMemenuhiSyarat(poinUser)) {
print('Dengan $poinUser poin, berhak atas: ${tingkat.label}');
}
}
}
Output:
Status: Member Emas (Diskon 12%)
Belanja Rp250.000 menjadi: Rp220000
Dengan 620 poin, berhak atas: Member Reguler
Dengan 620 poin, berhak atas: Member Perak
Dengan 620 poin, berhak atas: Member Emas
Menambah Kemampuan Tipe Bawaan dengan extension
Bagaimana jika kita ingin menambahkan method praktis seperti .keFormatRupiah() pada tipe int, atau .kapitalisasiKata() pada tipe String? Kita tentu tidak bisa mengedit kode sumber asli milik Dart SDK, dan kita juga tidak bisa melakukan extends int atau extends String.
Solusinya adalah extension. Dengan extension NamaExtension on TipeTarget, kita bisa "menyuntikkan" method dan getter pembantu ke tipe data mana pun—baik tipe bawaan Dart, koleksi List<T>, maupun kelas dari package pihak ketiga. Di dalam blok extension, gunakan kata kunci this untuk merujuk ke nilai objek yang sedang dipanggil.
extension FormatRupiahInt on int {
String keRupiah() {
final angkaStr = abs().toString();
final buffer = StringBuffer();
for (var i = 0; i < angkaStr.length; i++) {
if (i > 0 && (angkaStr.length - i) % 3 == 0) {
buffer.write('.');
}
buffer.write(angkaStr[i]);
}
final tanda = this < 0 ? '-Rp' : 'Rp';
return '$tanda$buffer';
}
}
extension ManipulasiTeks on String {
String get kapitalisasiAwalKata {
if (trim().isEmpty) return this;
return trim()
.split(RegExp(r'\s+'))
.map((kata) => '${kata[0].toUpperCase()}${kata.substring(1).toLowerCase()}')
.join(' ');
}
}
void main() {
final hargaLaptop = 14750000;
final namaPelanggan = ' mUHAMMAD rIZKY pRATAMA ';
// Memanggil method extension langsung seolah-olah bawaan int dan String!
print('Pelanggan : ${namaPelanggan.kapitalisasiAwalKata}');
print('Tagihan : ${hargaLaptop.keRupiah()}');
}
Output:
Pelanggan : Muhammad Rizky Pratama
Tagihan : Rp14.750.000
Coba sendiri
Di proyek VS Code Anda, buat file bin/uji_fitur_dart.dart:
- Salin kode
extension FormatRupiahIntdi atas ke dalam file tersebut. - Tambahkan satu getter baru di dalam
FormatRupiahInt:int get ppn11Persen => (this * 0.11).round(); - Di dalam
main(), tulis kode:final subtotal = 200000; print('Subtotal: ${subtotal.keRupiah()}'); print('PPN 11% : ${subtotal.ppn11Persen.keRupiah()}'); - Jalankan dengan
dart run bin/uji_fitur_dart.dartdan perhatikan betapa bersihnya pemanggilan berantaisubtotal.ppn11Persen.keRupiah().
Kesalahan yang sering terjadi
| Kesalahan Pemula | Mengapa Bermasalah? | Solusi yang Benar |
|---|---|---|
Menambahkan constructor biasa di dalam mixin |
mixin tidak dapat diinstansiasi secara mandiri sehingga Dart melarang deklarasi constructor di dalam mixin |
Inisialisasi state melalui method atau batasi dengan mixin ... on KelasInduk |
Lupa menuliskan titik koma ; setelah nilai konstanta terakhir pada Enhanced Enum |
Menyebabkan syntax error ketika kita menambahkan final field atau constructor di bawah daftar nilai enum |
Selalu ganti koma dengan titik koma ; tepat setelah konstanta enum terakhir |
Membuat field yang tidak final (bisa diubah-ubah) di dalam enum |
Semua nilai enum di Dart wajib berupa konstanta waktu kompilasi (const) |
Gunakan final pada seluruh properti di dalam enum |
Membuat extension tanpa nama (anonim) lalu kebingungan saat ingin mengekspornya ke file lain |
extension tanpa nama hanya terlihat di dalam file tempat ia ditulis (library-private) |
Selalu berikan nama berawalan huruf kapital (PascalCase), misalnya extension FormatTanggal on DateTime, agar bisa di-import dari file lain |
Kaitannya dengan Flutter / Dunia Nyata: Ketiga fitur ini akan Anda temui setiap hari saat mengembangkan aplikasi Flutter! Ketika Anda membuat animasi di Flutter, kelas
StateAnda wajib menggunakan mixin bawaan framework:class _LayarUtamaState extends State<LayarUtama> with SingleTickerProviderStateMixin. Selain itu, package populer di ekosistem Flutter sangat gemar menyediakanextensionpadaBuildContext(misalnyacontext.themeataucontext.mediaQuery) agar kode tampilan Anda jauh lebih ringkas dan enak dibaca.
Latihan
- Mixin Kemampuan Ekspor JSON: Buat sebuah
mixin KonversiMapyang memiliki kontrakMap<String, dynamic> keMap()dan method konkretString keJsonSederhana()yang mengubah isi map menjadi string berbentuk{"kunci": "nilai"}. Tempelkan mixin ini menggunakanwithpada kelasBarang(kode, nama)danKategori(id, judul), lalu cetak hasilkeJsonSederhana()keduanya. - Enhanced Enum Prioritas Tugas: Buat enhanced enum
PrioritasTugasdengan tiga konstanta:rendah,sedang, dantinggi. Masing-masing menyimpan propertifinal String label,final int batasHariSelesai, danfinal String simbol. Tambahkan methodString ringkasanTarget(String namaTugas)di dalam enum tersebut, lalu uji pada ketiganya. - Extension Koleksi Angka: Buat
extension StatistikListAngka on List<num>yang memiliki getternum get totalJumlah,double get rataRata(kembalikan0jika list kosong), dan methodList<num> diAtasRataRata(). Uji extension tersebut padafinal nilaiUjian = [75, 88, 92, 64, 81];.
Kunci jawaban dan pembahasan
Latihan 1
mixin KonversiMap {
// Method abstrak di dalam mixin: wajib diimplementasikan oleh kelas pemakai
Map<String, dynamic> keMap();
String keJsonSederhana() {
final pasangan = keMap()
.entries
.map((e) => '"${e.key}": "${e.value}"')
.join(', ');
return '{$pasangan}';
}
}
class Barang with KonversiMap {
final String kode;
final String nama;
Barang({required this.kode, required this.nama});
@override
Map<String, dynamic> keMap() => {'kode': kode, 'nama': nama};
}
class Kategori with KonversiMap {
final int id;
final String judul;
Kategori({required this.id, required this.judul});
@override
Map<String, dynamic> keMap() => {'id': id, 'judul': judul};
}
void main() {
final brg = Barang(kode: 'BRG-01', nama: 'Mouse Nirkabel');
final kat = Kategori(id: 10, judul: 'Aksesoris Komputer');
print(brg.keJsonSederhana());
print(kat.keJsonSederhana());
}
Pembahasan: Sebuah mixin boleh memiliki method abstrak (keMap()) sekaligus method konkret (keJsonSederhana()) yang bergantung pada method abstrak tersebut. Dengan begitu, kelas Barang dan Kategori cukup menyediakan keMap() untuk langsung mendapatkan fitur keJsonSederhana().
Latihan 2
enum PrioritasTugas {
rendah(label: 'Rendah', batasHariSelesai: 7, simbol: '[!]'),
sedang(label: 'Sedang', batasHariSelesai: 3, simbol: '[!!]'),
tinggi(label: 'Tinggi', batasHariSelesai: 1, simbol: '[!!!]');
final String label;
final int batasHariSelesai;
final String simbol;
const PrioritasTugas({
required this.label,
required this.batasHariSelesai,
required this.simbol,
});
String ringkasanTarget(String namaTugas) {
return '$simbol Tugas "$namaTugas" (Prioritas $label) -> Target selesai dalam $batasHariSelesai hari.';
}
}
void main() {
print(PrioritasTugas.tinggi.ringkasanTarget('Perbaikan Bug Pembayaran'));
print(PrioritasTugas.sedang.ringkasanTarget('Rapikan Dokumentasi API'));
print(PrioritasTugas.rendah.ringkasanTarget('Ganti Warna Ikon Footer'));
}
Pembahasan: Enhanced Enum mengelompokkan data konstan beserta perilaku terkaitnya di satu tempat. Kita tidak perlu lagi menulis switch-case terpisah di luar enum hanya untuk mencari tahu berapa batas hari atau simbol dari setiap tingkat prioritas.
Latihan 3
extension StatistikListAngka on List<num> {
num get totalJumlah => fold(0, (akumulasi, elemen) => akumulasi + elemen);
double get rataRata => isEmpty ? 0.0 : totalJumlah / length;
List<num> diAtasRataRata() {
final batas = rataRata;
return where((nilai) => nilai > batas).toList();
}
}
void main() {
final nilaiUjian = <num>[75, 88, 92, 64, 81];
print('Total nilai : ${nilaiUjian.totalJumlah}');
print('Rata-rata kelas : ${nilaiUjian.rataRata.toStringAsFixed(1)}');
print('Di atas rata-rata : ${nilaiUjian.diAtasRataRata()}');
}
Pembahasan: Di dalam extension StatistikListAngka on List<num>, kita bisa langsung memanggil properti dan method milik List seperti isEmpty, length, fold(), dan where() karena this merujuk langsung ke objek List<num> yang sedang dioperasikan.
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.