Ada satu kebiasaan koding yang diam-diam menciptakan banyak noise di codebase: fungsi utilitas yang tersebar di berbagai file helper, dipanggil dengan sintaks yang tidak intuitif, dan sering diduplikasi karena developer tidak tahu sudah ada di file lain.
Extension methods adalah solusi Dart untuk masalah ini β cara untuk menambahkan method ke tipe yang sudah ada tanpa mengubah kelas aslinya atau menggunakan inheritance.
Setelah menggunakannya secara konsisten selama dua tahun terakhir, cara pandang saya terhadap utility code di Flutter berubah cukup fundamental.
Sintaks Dasar
// Tanpa extension β fungsi helper yang verbose
String capitalizeFirst(String s) {
if (s.isEmpty) return s;
return s[0].toUpperCase() + s.substring(1);
}
// Penggunaan:
final nama = capitalizeFirst('budi santoso');
// Dengan extension β terasa seperti method bawaan String
extension StringExtension on String {
String get capitalizeFirst {
if (isEmpty) return this;
return this[0].toUpperCase() + substring(1);
}
String capitalizeWords() {
return split(' ').map((word) => word.capitalizeFirst).join(' ');
}
}
// Penggunaan:
final nama = 'budi santoso'.capitalizeWords(); // 'Budi Santoso'
Perbedaannya lebih dari sekadar estetika. Dengan extension, IDE auto-complete akan menyarankan method tersebut ketika Anda mengetik di variable bertipe String. Tidak perlu ingat nama file helper-nya, tidak perlu import khusus jika sudah dalam package yang sama.
Extension untuk Tipe Flutter yang Sering Saya Gunakan
BuildContext extensions β mengurangi boilerplate akses theme dan navigator:
extension ContextExtension on BuildContext {
// Theme shortcuts
ThemeData get theme => Theme.of(this);
TextTheme get textTheme => Theme.of(this).textTheme;
ColorScheme get colors => Theme.of(this).colorScheme;
// Media query shortcuts
Size get screenSize => MediaQuery.sizeOf(this);
double get screenWidth => MediaQuery.sizeOf(this).width;
double get screenHeight => MediaQuery.sizeOf(this).height;
bool get isKeyboardVisible => MediaQuery.viewInsetsOf(this).bottom > 0;
// Navigation shortcuts
NavigatorState get navigator => Navigator.of(this);
void pop<T>([T? result]) => Navigator.of(this).pop(result);
Future push(Widget page) => Navigator.of(this).push(
MaterialPageRoute(builder: (_) => page),
);
// SnackBar shortcut
void showSnackBar(String pesan, {bool isError = false}) {
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
content: Text(pesan),
backgroundColor: isError ? colors.error : null,
),
);
}
}
// Penggunaan yang jauh lebih bersih:
context.showSnackBar('Data berhasil disimpan');
final lebar = context.screenWidth;
context.pop();
DateTime extensions untuk format yang konsisten:
extension DateTimeExtension on DateTime {
String get formatTanggal {
final bulan = [
'', 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
];
return '$day ${bulan[month]} $year';
}
String get formatWaktu => '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
String get formatLengkap => '$formatTanggal, $formatWaktu WIB';
bool get isToday {
final now = DateTime.now();
return year == now.year && month == now.month && day == now.day;
}
String get relatif {
final selisih = DateTime.now().difference(this);
if (selisih.inMinutes < 1) return 'baru saja';
if (selisih.inHours < 1) return '${selisih.inMinutes} menit lalu';
if (selisih.inDays < 1) return '${selisih.inHours} jam lalu';
if (selisih.inDays < 7) return '${selisih.inDays} hari lalu';
return formatTanggal;
}
}
// Penggunaan:
Text(artikel.tanggalDibuat.relatif)
Text(transaksi.waktu.formatLengkap)
int dan double extensions untuk format angka:
extension IntExtension on int {
String get formatRupiah {
final formatter = NumberFormat.currency(
locale: 'id_ID',
symbol: 'Rp ',
decimalDigits: 0,
);
return formatter.format(this);
}
Duration get detik => Duration(seconds: this);
Duration get menit => Duration(minutes: this);
SizedBox get heightBox => SizedBox(height: toDouble());
SizedBox get widthBox => SizedBox(width: toDouble());
}
// Penggunaan:
Text(harga.formatRupiah) // 'Rp 150.000'
Column(children: [
widget1,
16.heightBox, // SizedBox(height: 16)
widget2,
])
Extension pada Generic Type
Extension bisa juga didefinisikan pada generic type, yang memungkinkan penambahan method yang type-safe untuk semua spesialisasi tipe tersebut:
extension ListExtension<T> on List<T> {
// Bagi list menjadi chunks berukuran tertentu
List<List<T>> chunked(int ukuran) {
final chunks = <List<T>>[];
for (var i = 0; i < length; i += ukuran) {
chunks.add(sublist(i, (i + ukuran).clamp(0, length)));
}
return chunks;
}
// Versi aman dari firstWhere β mengembalikan null jika tidak ditemukan
T? firstWhereOrNull(bool Function(T) test) {
for (final element in this) {
if (test(element)) return element;
}
return null;
}
// Hapus duplikat berdasarkan selector
List<T> uniqueBy<K>(K Function(T) selector) {
final seen = <K>{};
return where((item) => seen.add(selector(item))).toList();
}
}
// Penggunaan:
final grid = items.chunked(3); // Untuk grid layout 3 kolom
final artikel = daftar.firstWhereOrNull((a) => a.id == targetId);
final unik = semua.uniqueBy((item) => item.kategoriId);
Extension pada Nullable Type
Extension bisa didefinisikan pada nullable type β sesuatu yang tidak bisa dilakukan dengan cara lain:
extension NullableStringExtension on String? {
bool get isNullOrEmpty => this == null || this!.isEmpty;
bool get isNotNullOrEmpty => !isNullOrEmpty;
String get orEmpty => this ?? '';
String get orTidakDiketahui => this ?? 'Tidak diketahui';
}
// Penggunaan:
if (namaUser.isNullOrEmpty) {
// Tampilkan placeholder
}
Text(pengguna.bio.orEmpty)
Text(pengguna.kota.orTidakDiketahui)
Batasan yang Perlu Diketahui
Extension methods bukan tanpa limitasi:
Tidak bisa menambahkan field/property dengan state. Extension hanya bisa menambahkan method dan getter yang komputasinya murni dari data yang sudah ada. Anda tidak bisa menambahkan field baru ke tipe yang di-extend.
Konflik nama bisa terjadi. Jika ada dua extension yang mendefinisikan method dengan nama sama pada tipe yang sama, Dart akan meminta Anda untuk eksplisit tentang extension mana yang digunakan. Ini bisa membingungkan jika tidak dikelola dengan baik.
Tidak berlaku pada interface inheritance. Extension method yang ditambahkan ke String tidak otomatis tersedia jika seseorang membuat class yang implement String (yang memang tidak mungkin, tapi konsep ini berlaku untuk interface lain).
Cara Mengorganisir Extension dalam Proyek
Saya biasanya mengorganisir extension dalam satu folder lib/core/extensions/ dengan satu file per tipe yang di-extend:
lib/core/extensions/
βββ string_extension.dart
βββ datetime_extension.dart
βββ context_extension.dart
βββ list_extension.dart
βββ num_extension.dart
Dan satu file barrel untuk export semua:
// lib/core/extensions/extensions.dart
export 'string_extension.dart';
export 'datetime_extension.dart';
export 'context_extension.dart';
export 'list_extension.dart';
export 'num_extension.dart';
Satu import di setiap file yang membutuhkannya, dan semua extension tersedia.
Kesimpulan
Extension methods bukan fitur yang mengubah cara Anda menulis logika bisnis. Tapi mereka secara dramatis mengurangi noise syntactic yang membuat kode susah dibaca.
Kode yang baik bukan hanya kode yang benar β tapi kode yang niatnya langsung terbaca. Dan artikel.tanggal.relatif mengkomunikasikan niat jauh lebih jelas dari DateTimeHelper.formatRelative(artikel.tanggal).
Itulah nilainya.
Belum ada komentar. Jadilah yang pertama menulis.
Tulis Komentar