Pemrograman Dart
Generics (<T>) dan Penanganan Error (try, on, catch, finally)
Pelajari cara menulis kelas dan fungsi yang fleksibel namun tetap type-safe menggunakan Generics (<T>), serta menangani kegagalan program dengan try, on, catch, finally, dan Custom Exception.

Ketika aplikasi kita tumbuh semakin kompleks, ada dua tantangan rekayasa perangkat lunak yang pasti muncul. Pertama, bagaimana cara menulis kelas atau fungsi yang dapat bekerja untuk berbagai tipe data (misalnya pembungkus respons Kotak<String>, Kotak<int>, atau Kotak<Produk>) tanpa mengorbankan keamanan tipe dengan memakai dynamic? Kedua, bagaimana cara mengantisipasi situasi tak terduga saat program berjalan—seperti input angka yang tidak valid, stok barang habis, atau koneksi terputus—agar aplikasi tidak langsung berhenti mendadak (crash)? Pada materi ini, kita akan mempelajari Generics (<T>) untuk menjaga fleksibilitas tipe data dan Exception Handling untuk menangani kegagalan secara elegan.
Yang akan dipelajari
- Membuat kelas dan fungsi generik menggunakan parameter tipe (
<T>) agar kode dapat digunakan ulang dengan aman (type-safe). - Membatasi parameter tipe generik menggunakan
T extends TipeInduk. - Membedakan antara
Error(kesalahan logika pemrogram) danException(kondisi gagal yang wajib ditangani). - Menangkap dan memulihkan kegagalan menggunakan blok
try,on,catch,finally, serta kata kuncithrowdanrethrow. - Membuat kelas Custom Exception sendiri yang informatif.
Memahami Generics (<T>)
Sebenarnya Anda sudah memakai Generics sejak Bab 2 ketika menulis List<String> atau Map<String, int>. Tanda kurung sudut <...> memberi tahu compiler Dart tipe data apa yang diizinkan masuk ke dalam koleksi tersebut.
Mengapa kita tidak memakai Object atau dynamic saja jika ingin kelas kita menerima tipe apa pun? Perhatikan perbandingannya:
| Pendekatan | Fleksibel untuk Berbagai Tipe? | Pemeriksaan Tipe Saat Kompilasi? | Perlu Type Casting (as) Manual? |
|---|---|---|---|
Tipe spesifik (KotakString) |
Tidak (harus duplikasi kelas tiap tipe) | Ya | Tidak |
Memakai dynamic |
Ya | Tidak (rawan runtime error) | Ya / rawan salah panggil method |
Generics (Kotak<T>) |
Ya | Ya (100% type-safe) | Tidak |
Huruf T (singkatan dari Type) hanyalah konvensi penamaan parameter tipe. Anda juga sering melihat E (Element), K (Key), dan V (Value).
Membuat Kelas dan Fungsi Generik
Mari kita buat kelas ResponApi<T> yang dapat membungkus data hasil operasi apa pun—mulai dari profil pengguna hingga daftar angka—beserta sebuah fungsi generik ambilElemenTengah<T>:
class ResponApi<T> {
final bool sukses;
final String pesan;
final T? data;
const ResponApi({
required this.sukses,
required this.pesan,
this.data,
});
void tampilkanRingkasan() {
if (sukses && data != null) {
print('[SUKSES] $pesan -> Isi data (${data.runtimeType}): $data');
} else {
print('[GAGAL] $pesan');
}
}
}
// Fungsi generik yang menerima List<T> dan mengembalikan T
T ambilElemenTengah<T>(List<T> daftar) {
final indeksTengah = daftar.length ~/ 2;
return daftar[indeksTengah];
}
void main() {
// T otomatis dikunci sebagai String
const responToken = ResponApi<String>(
sukses: true,
pesan: 'Token berhasil dibuat',
data: 'eyJhbGciOiJIUzI1NiIs',
);
// T otomatis dikunci sebagai List<int>
const responSkor = ResponApi<List<int>>(
sukses: true,
pesan: 'Data skor dimuat',
data: [85, 90, 95],
);
responToken.tampilkanRingkasan();
responSkor.tampilkanRingkasan();
// Karena type-safe, responToken.data langsung dikenali sebagai String?
print('Panjang token: ${responToken.data?.length} karakter');
final kotaTengah = ambilElemenTengah<String>(['Jakarta', 'Bandung', 'Surabaya']);
print('Kota di tengah: $kotaTengah');
}
Output:
[SUKSES] Token berhasil dibuat -> Isi data (String): eyJhbGciOiJIUzI1NiIs
[SUKSES] Data skor dimuat -> Isi data (List<int>): [85, 90, 95]
Panjang token: 20 karakter
Kota di tengah: Bandung
Membatasi Tipe Generik dengan extends
Terkadang kita tidak ingin <T> diisi dengan sembarang tipe. Misalnya, kita ingin membuat kelas GudangItem<T> yang hanya boleh menampung objek turunan dari BarangTerdaftar agar kita pasti bisa mengakses properti .id miliknya. Kita dapat membatasinya dengan <T extends BarangTerdaftar>:
abstract class BarangTerdaftar {
String get kodeBarang;
String get namaBarang;
}
class Elektronik extends BarangTerdaftar {
@override
final String kodeBarang;
@override
final String namaBarang;
final int garansiBulan;
Elektronik({
required this.kodeBarang,
required this.namaBarang,
required this.garansiBulan,
});
}
// T dibatasi HANYA untuk BarangTerdaftar atau subclass-nya
class GudangInventaris<T extends BarangTerdaftar> {
final Map<String, T> _koleksi = {};
void tambah(T item) {
// Aman mengakses item.kodeBarang karena T pasti BarangTerdaftar!
_koleksi[item.kodeBarang] = item;
}
T? cariBerdasarkanKode(String kode) => _koleksi[kode];
}
void main() {
final gudangElektronik = GudangInventaris<Elektronik>();
gudangElektronik.tambah(
Elektronik(kodeBarang: 'EL-01', namaBarang: 'Monitor 27 Inci', garansiBulan: 24),
);
final hasil = gudangElektronik.cariBerdasarkanKode('EL-01');
print('Ditemukan: ${hasil?.namaBarang} (Garansi ${hasil?.garansiBulan} bulan)');
// final gudangSalah = GudangInventaris<String>(); // ERROR KOMPILASI! String bukan BarangTerdaftar.
}
Output:
Ditemukan: Monitor 27 Inci (Garansi 24 bulan)
Penanganan Error: Error vs Exception
Di Dart, masalah yang terjadi saat program berjalan (runtime) dibagi menjadi dua keluarga besar:
| Kategori | Contoh Kelas Bawaan | Penyebab Utama | Apakah Perlu Di-try-catch? |
|---|---|---|---|
Error |
RangeError, StackOverflowError, TypeError |
Bug logika kode pemrogram (misal mengakses indeks ke-10 pada List berisi 3 elemen) |
Tidak. Perbaiki logika kodenya (misal cek panjang List terlebih dahulu) |
Exception |
FormatException, IOException, TimeoutException |
Kondisi eksternal/input yang tidak ideal tetapi sudah diantisipasi (misal input user bukan angka, file tidak ditemukan) | Ya, wajib ditangkap dengan try-catch agar aplikasi memberikan pesan ramah ke pengguna |
Struktur try, on, catch, finally, dan rethrow
Untuk melempar sinyal bahwa sebuah fungsi mengalami kondisi tidak normal, kita menggunakan kata kunci throw. Untuk menangkap sinyal tersebut agar program tidak crash, kita membungkus pemanggilannya di dalam blok try:
on NamaException: menangkap tipe exception tertentu secara spesifik.catch (e, stackTrace): menangkap objek exception (e) dan jejak pemanggilan fungsi (stackTrace). Bisa digabungkan menjadion FormatException catch (e).finally: blok kode yang selalu dijalankan di akhir, baik terjadi exception maupun tidak (cocok untuk menutup koneksi atau membersihkan sumber daya).rethrow: melempar kembali exception yang baru saja ditangkap di dalam blokcatchagar pemanggil di level atas tetap mengetahuinya (misalnya setelah kita mencatat log error).
Membuat Custom Exception Sendiri
Praktik terbaik di Dart ketika membangun logika bisnis adalah membuat kelas Custom Exception yang mengimplementasikan Exception. Dengan begitu, kita bisa membedakan jenis kesalahan secara presisi menggunakan klausa on:
class SaldoTidakCukupException implements Exception {
final int saldoSaatIni;
final int nominalPenarikan;
const SaldoTidakCukupException({
required this.saldoSaatIni,
required this.nominalPenarikan,
});
int get kekurangan => nominalPenarikan - saldoSaatIni;
@override
String toString() {
return 'SaldoTidakCukupException: Saldo Rp$saldoSaatIni tidak cukup untuk menarik Rp$nominalPenarikan (kurang Rp$kekurangan).';
}
}
class NominalTidakValidException implements Exception {
final String pesan;
const NominalTidakValidException(this.pesan);
@override
String toString() => 'NominalTidakValidException: $pesan';
}
class MesinATM {
int _saldo = 500000;
void tarikTunai(String inputNominalTeks) {
// int.parse akan melempar FormatException jika teks bukan angka bulat
final nominal = int.parse(inputNominalTeks);
if (nominal <= 0 || nominal % 50000 != 0) {
throw const NominalTidakValidException(
'Nominal penarikan harus positif dan kelipatan Rp50.000.',
);
}
if (nominal > _saldo) {
throw SaldoTidakCukupException(
saldoSaatIni: _saldo,
nominalPenarikan: nominal,
);
}
_saldo -= nominal;
print('Penarikan Rp$nominal berhasil! Sisa saldo: Rp$_saldo');
}
}
void ujiTransaksiATM(MesinATM atm, String input) {
print('\n--- Mencoba tarik tunai dengan input: "$input" ---');
try {
atm.tarikTunai(input);
} on FormatException {
print('Peringatan UI: Masukkan angka saja tanpa huruf atau simbol.');
} on NominalTidakValidException catch (e) {
print('Peringatan UI: ${e.pesan}');
} on SaldoTidakCukupException catch (e) {
print('Peringatan UI: Maaf, saldo Anda kurang Rp${e.kekurangan}.');
} catch (e) {
print('Terjadi kesalahan tak terduga: $e');
} finally {
print('[Sistem] Kartu ATM siap dikeluarkan / sesi diperbarui.');
}
}
void main() {
final atm = MesinATM();
ujiTransaksiATM(atm, '200000'); // Berhasil
ujiTransaksiATM(atm, 'dua ratus'); // Memicu FormatException
ujiTransaksiATM(atm, '75000'); // Memicu NominalTidakValidException
ujiTransaksiATM(atm, '400000'); // Memicu SaldoTidakCukupException
}
Output:
--- Mencoba tarik tunai dengan input: "200000" ---
Penarikan Rp200000 berhasil! Sisa saldo: Rp300000
[Sistem] Kartu ATM siap dikeluarkan / sesi diperbarui.
--- Mencoba tarik tunai dengan input: "dua ratus" ---
Peringatan UI: Masukkan angka saja tanpa huruf atau simbol.
[Sistem] Kartu ATM siap dikeluarkan / sesi diperbarui.
--- Mencoba tarik tunai dengan input: "75000" ---
Peringatan UI: Nominal penarikan harus positif dan kelipatan Rp50.000.
[Sistem] Kartu ATM siap dikeluarkan / sesi diperbarui.
--- Mencoba tarik tunai dengan input: "400000" ---
Peringatan UI: Maaf, saldo Anda kurang Rp100000.
[Sistem] Kartu ATM siap dikeluarkan / sesi diperbarui.
Coba sendiri
Buka proyek VS Code Anda dan buat file bin/uji_exception.dart:
- Salin kode
MesinATMdi atas dan jalankandart run bin/uji_exception.dart. - Di dalam blok
on SaldoTidakCukupException catch (e), tambahkan kata kuncirethrow;tepat di bawah barisprint(...). - Bungkus pemanggilan
ujiTransaksiATM(atm, '400000');di dalammain()dengan bloktry { ... } catch (e) { print('Log Pusat Bank mencatat kegagalan: $e'); }. - Jalankan kembali perintah
dart run bin/uji_exception.dartdan amati bagaimanarethrowmemungkinkan layar ATM menampilkan pesan ke pengguna sekaligus meneruskan laporan exception ke sistem pusat dimain()!
Kesalahan yang sering terjadi
| Kesalahan Pemula | Mengapa Bermasalah? | Solusi yang Benar |
|---|---|---|
Menulis blok catch (e) {} kosong tanpa isi (swallowing exception) |
Kesalahan hilang tanpa jejak, sehingga ketika program gagal bekerja kita sangat kesulitan melacak penyebabnya | Minimal tampilkan pesan ke pengguna, catat log error, atau teruskan dengan rethrow |
Menempatkan catch (e) umum di urutan paling atas sebelum on SpesifikException |
Karena catch (e) menangkap semua jenis objek, blok on yang ada di bawahnya menjadi kode mati (dead code) yang tidak pernah tercapai |
Urutkan klausa on dari yang paling spesifik di atas, lalu tutup dengan catch (e) umum di paling bawah |
Melempar string biasa seperti throw 'Gagal menyimpan'; |
Pemanggil fungsi tidak bisa memfilter jenis kesalahan dengan on TipeException dan tidak memiliki properti terstruktur |
Selalu lempar objek dari kelas yang mengimplementasikan Exception |
Menggunakan List<dynamic> padahal tipe elemennya seragam |
Kehilangan fitur auto-complete di VS Code dan pemeriksaan tipe saat kompilasi | Gunakan tipe generik eksplisit seperti List<Produk> atau buat kelas generik <T> |
Kaitannya dengan Flutter / Dunia Nyata: Di Flutter, setiap kali Anda berpindah halaman dan mengirim atau menerima nilai kembali, Anda menggunakan Generics:
Navigator.push<HasilPilihan>(context, MaterialPageRoute(...)). Begitu pulaFuture<Pengguna>atauState<HalamanProfil>semuanya dibangun di atas Generics agar compiler selalu menjaga keamanan tipe komponen Anda. Sementara itu, Exception Handling dengantry-on-catch-finallyadalah pola wajib saat aplikasi Flutter melakukan panggilan HTTP ke server: bloktrymemulai indikator loading dan memanggil API, blokon SocketExceptionmemberi tahu pengguna bahwa internet terputus, dan blokfinallymematikan indikator loading agar layar tidak membeku.
Latihan
- Kelas Generik
Pasangan<K, V>: Buat sebuah kelas generikPasangan<K, V>dengan dua parameter tipe (Kuntuk kunci danVuntuk nilai). Tambahkan methodPasangan<V, K> tukarPosisi()yang mengembalikan objekPasanganbaru dengan posisi kunci dan nilai yang dibalik, serta overridetoString(). Uji denganPasangan<String, int>('Juara', 1). - Fungsi Generik Terbatas (
T extends Comparable<T>): Buat fungsi generikT cariNilaiMaksimum<T extends Comparable<T>>(List<T> daftar)yang mencari nilai terbesar menggunakan method.compareTo()(jikaelemen.compareTo(maks) > 0, makaelemenlebih besar). Jikadaftarkosong, lemparArgumentError('Daftar tidak boleh kosong'). Uji fungsi tersebut padaList<num>danList<String>. - Custom Exception Pemesanan Tiket: Buat kelas
StokTiketHabisException implements Exceptionyang menyimpannamaKonser,jumlahDiminta, dansisaKursi. Buat kelasLoketKonserdengan methodvoid pesanTiket(int jumlah)yang melempar exception tersebut jikajumlah > sisaKursi. Tangkap dengan bloktry - on StokTiketHabisException catch (e) - finallydi dalammain().
Kunci jawaban dan pembahasan
Latihan 1
class Pasangan<K, V> {
final K kunci;
final V nilai;
const Pasangan(this.kunci, this.nilai);
Pasangan<V, K> tukarPosisi() {
return Pasangan<V, K>(nilai, kunci);
}
@override
String toString() => 'Pasangan<$K, $V>(kunci: $kunci, nilai: $nilai)';
}
void main() {
const awal = Pasangan<String, int>('Juara', 1);
final hasilTukar = awal.tukarPosisi();
print(awal);
print(hasilTukar);
}
Pembahasan: Sebuah kelas generik boleh menerima lebih dari satu parameter tipe (<K, V>). Ketika tukarPosisi() dipanggil pada Pasangan<String, int>, tipe kembaliannya secara otomatis diketahui oleh compiler sebagai Pasangan<int, String>.
Latihan 2
T cariNilaiMaksimum<T extends Comparable<T>>(List<T> daftar) {
if (daftar.isEmpty) {
throw ArgumentError('Daftar tidak boleh kosong');
}
var maksimum = daftar.first;
for (final elemen in daftar.skip(1)) {
if (elemen.compareTo(maksimum) > 0) {
maksimum = elemen;
}
}
return maksimum;
}
void main() {
final angka = <num>[42, 19, 88, 73, 55];
final nama = <String>['Budi', 'Zaki', 'Andi', 'Citra'];
print('Angka terbesar : ${cariNilaiMaksimum(angka)}');
print('Urutan abjad terakhir : ${cariNilaiMaksimum(nama)}');
}
Pembahasan: Tipe num dan String di Dart sama-sama mengimplementasikan Comparable. Dengan batasan <T extends Comparable<T>>, kita mendapatkan kepastian bahwa setiap elemen T memiliki method .compareTo(), sehingga satu fungsi yang sama bisa membandingkan angka maupun urutan abjad teks secara type-safe.
Latihan 3
class StokTiketHabisException implements Exception {
final String namaKonser;
final int jumlahDiminta;
final int sisaKursi;
const StokTiketHabisException({
required this.namaKonser,
required this.jumlahDiminta,
required this.sisaKursi,
});
@override
String toString() {
return 'Gagal memesan $jumlahDiminta tiket "$namaKonser". Kursi tersisa hanya $sisaKursi.';
}
}
class LoketKonser {
final String namaKonser;
int _sisaKursi;
LoketKonser({required this.namaKonser, required int kuotaAwal})
: _sisaKursi = kuotaAwal;
void pesanTiket(int jumlah) {
if (jumlah > _sisaKursi) {
throw StokTiketHabisException(
namaKonser: namaKonser,
jumlahDiminta: jumlah,
sisaKursi: _sisaKursi,
);
}
_sisaKursi -= jumlah;
print('Berhasil memesan $jumlah tiket "$namaKonser". Sisa kursi: $_sisaKursi');
}
}
void main() {
final loket = LoketKonser(namaKonser: 'Simfoni Nusantara', kuotaAwal: 4);
try {
loket.pesanTiket(2);
loket.pesanTiket(3); // Akan memicu StokTiketHabisException
} on StokTiketHabisException catch (e) {
print('[Peringatan Loket] $e');
} finally {
print('[Selesai] Terima kasih telah mengunjungi loket tiket.');
}
}
Pembahasan: Dengan menyimpan detail konteks kesalahan (namaKonser, jumlahDiminta, sisaKursi) sebagai properti di dalam StokTiketHabisException, bagian penangkap error (on StokTiketHabisException catch (e)) dapat menyusun pesan yang jelas dan spesifik bagi calon pembeli.
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.