OOP di Dart #
Dart dirancang sejak awal sebagai bahasa pemrograman berorientasi objek murni (pure object-oriented programming language). Di dalam ekosistem Dart, setiap nilai adalah sebuah objek — termasuk tipe dasar seperti angka, fungsi, dan bahkan nilai null. Setiap objek merupakan instansi dari suatu kelas, dan semua kelas (kecuali tipe Null) bernaung di bawah satu kelas induk utama yaitu Object. Memahami pilar-pilar OOP di Dart secara mendalam adalah kunci utama untuk mendesain arsitektur aplikasi Flutter yang modular, terstruktur, mudah diuji, serta dapat dipelihara dalam jangka panjang.
Konsep Kelas, Objek, dan Enkapsulasi #
Kelas (Class) adalah sebuah cetak biru (blueprint) atau definisi tipe data kustom yang menjelaskan karakteristik data dan perilaku perilaku dari suatu entitas. Objek (Object) adalah realisasi konkret atau instansi (instance) fisik dari kelas tersebut yang menempati ruang di memori heap aplikasi.
Enkapsulasi di Dart #
Enkapsulasi adalah proses pembungkusan data (variabel instansi) dan perilaku (metode) dalam satu unit tunggal, sekaligus membatasi akses langsung dari luar objek untuk menjaga integritas status internal.
Berbeda dengan bahasa seperti Java atau C++ yang menggunakan kata kunci akses kontrol eksplisit seperti public, private, atau protected, Dart menggunakan pendekatan yang lebih sederhana berbasis pustaka (library-level privacy):
- Anggota kelas yang bersifat umum (public) ditulis seperti variabel biasa.
- Anggota kelas yang bersifat privat (private) ditandai dengan menambahkan garis bawah (
_) di depan nama variabel atau metode tersebut. Anggota privat ini hanya dapat diakses oleh kode yang berada di dalam file (library) yang sama.
Mari kita perhatikan implementasi enkapsulasi yang benar dengan menggunakan getter dan setter untuk menjaga integritas data:
// ANTI-PATTERN: Mengizinkan modifikasi data internal secara bebas dari luar kelas
class BadProduct {
String name;
double price; // Rawan diubah menjadi nilai negatif dari luar!
BadProduct(this.name, this.price);
}
// ====================================================================
// BENAR: Menggunakan enkapsulasi, variabel privat, dan validasi via setter
class Product {
final String id;
final String name;
double _price; // Privat: Hanya dapat diakses di file ini
Product(this.id, this.name, double price) : _price = price {
// Validasi saat inisiasi objek
if (price < 0) throw ArgumentError('Harga tidak boleh negatif');
}
// Getter: Memberikan akses baca terkontrol ke dunia luar
double get price => _price;
// Setter: Memvalidasi setiap upaya perubahan nilai harga
set price(double newPrice) {
if (newPrice < 0) {
throw ArgumentError('Harga baru tidak boleh negatif');
}
_price = newPrice;
}
// Metode untuk memodifikasi status internal secara aman
void applyDiscount(double percentage) {
if (percentage < 0 || percentage > 100) {
throw ArgumentError('Persentase diskon tidak valid');
}
_price = _price * (1 - percentage / 100);
}
}
Membedah 5 Jenis Konstruktor di Dart #
Konstruktor (Constructor) adalah metode khusus yang dipanggil pertama kali saat kita membuat instansi objek baru dari suatu kelas. Dart menawarkan fleksibilitas yang sangat tinggi dengan menyediakan 5 jenis konstruktor yang berbeda:
1. Generative Constructor #
Konstruktor standar yang paling sering kita gunakan untuk membuat instansi objek baru dan langsung menetapkan nilai parameter ke variabel instansi.
class Point {
final double x;
final double y;
// Menggunakan sintaksis pemendek 'this'
Point(this.x, this.y);
}
2. Named Constructor #
Memungkinkan kita mendefinisikan beberapa konstruktor dengan nama berbeda di dalam satu kelas yang sama untuk mempermudah inisiasi objek dengan berbagai skenario data.
class Point {
final double x;
final double y;
Point(this.x, this.y);
// Named constructor: Membuat titik di koordinat nol (origin)
Point.origin() : x = 0, y = 0;
// Named constructor: Membuat objek dari data JSON
Point.fromJson(Map<String, dynamic> json)
: x = json['x'] as double,
y = json['y'] as double;
}
3. Const Constructor #
Jika kelas kita menyimpan data yang bersifat imut (immutable / seluruh field ditandai final), kita bisa menambahkan kata kunci const di depan konstruktor. Objek yang dibuat dengan const akan menjadi konstanta compile-time.
Di Flutter, penggunaan const sangat krusial pada widget tree karena memberi tahu Flutter bahwa widget tersebut tidak perlu di-rebuild ulang saat terjadi render ulang layar, meningkatkan performa rendering secara signifikan.
class ThemeColor {
final int hex;
// Membuat objek konstanta compile-time
const ThemeColor(this.hex);
}
void testConst() {
const color1 = ThemeColor(0xFFFFFFFF);
const color2 = ThemeColor(0xFFFFFFFF);
// Keduanya menunjuk ke referensi memori yang persis sama (canonical instance)
print(identical(color1, color2)); // Output: true
}
4. Factory Constructor #
Konstruktor yang menggunakan kata kunci factory memberikan kendali penuh kepada kita terhadap pembuatan objek. Berbeda dengan konstruktor generatif biasa yang wajib membuat objek baru, konstruktor factory dapat mengembalikan objek yang diambil dari cache memori, atau bahkan mengembalikan instansi dari sub-kelas yang berbeda.
Visualisasi perbedaan alur pembuatan objek dapat digambarkan sebagai berikut:
flowchart TD
Client["Pemanggil: Pembuatan Objek"] --> Choice{"Pilih Konstruktor"}
Choice -->|"Generative Constructor"| Gen["Alokasikan Memori Baru di Heap"]
Gen --> Init["Jalankan Initializer List & Constructor Body"]
Init --> ReturnNew["Kembalikan Objek Baru"]
Choice -->|"Factory Constructor"| Fact{"Kondisi Pembuatan"}
Fact -->|"Ada di Cache"| Cache["Ambil Objek dari Cache"]
Fact -->|"Buat Baru / Sub-kelas"| NewSub["Buat Instansi Baru / Sub-kelas"]
Cache & NewSub --> ReturnFact["Kembalikan Instansi yang Sesuai"]Mari kita perhatikan contoh pembuatan logger cache menggunakan factory:
class Logger {
final String name;
static final Map<String, Logger> _cache = {};
// Konstruktor generatif privat internal
Logger._internal(this.name);
// Factory constructor mengontrol pengembalian objek
factory Logger(String name) {
// Kembalikan objek dari cache jika sudah pernah dibuat sebelumnya
return _cache.putIfAbsent(name, () => Logger._internal(name));
}
}
5. Redirecting Constructor #
Konstruktor yang tidak memiliki tubuh kode (body) sendiri, melainkan hanya mendelegasikan tugas inisiasi objek ke konstruktor lain di dalam kelas yang sama untuk menghindari duplikasi kode.
class Point {
final double x;
final double y;
Point(this.x, this.y);
// Mengalihkan inisiasi titik koordinat y ke konstruktor utama
Point.horizontal(double x) : this(x, 0);
}
Initializer List dan Assertions #
Initializer list adalah daftar ekspresi yang dieksekusi sebelum tubuh konstruktor dijalankan. Ini sangat ideal untuk melakukan validasi input menggunakan assert atau menetapkan nilai field final berdasarkan perhitungan parameter:
class Circle {
final double radius;
final double area;
// Initializer list memvalidasi data dan menghitung luas area
Circle(double r)
: assert(r > 0, 'Jari-jari lingkaran harus positif'),
radius = r,
area = 3.14 * r * r;
}
Pewarisan Tunggal (Inheritance) #
Pewarisan (Inheritance) memungkinkan sebuah kelas baru (disebut sub-class atau child class) untuk mewarisi properti dan metode dari kelas yang sudah ada (disebut base class atau parent class) menggunakan kata kunci extends.
Dart menganut sistem Single Inheritance, artinya sebuah kelas hanya diperbolehkan mewarisi dari maksimal satu kelas induk secara langsung. Ini dilakukan untuk menghindari konflik kode nama metode yang sama dari dua induk yang berbeda (Diamond Problem).
Penggunaan super dan Super Parameters #
Subclass dapat mengakses dan memodifikasi metode induk dengan kata kunci super. Sejak Dart versi 2.17, kita bisa memanfaatkan fitur Super Parameters untuk langsung meneruskan nilai parameter konstruktor subclass ke parent class secara ringkas:
// Base Class
class Employee {
final String name;
final double salary;
Employee(this.name, this.salary);
void work() => print('$name sedang bekerja.');
}
// Subclass
class Developer extends Employee {
final String programmingLanguage;
// Menggunakan 'super.name' dan 'super.salary' untuk meneruskan ke parent constructor
Developer({
required super.name,
required super.salary,
required this.programmingLanguage,
});
// Melakukan override metode parent
@override
void work() {
super.work(); // Menjalankan logika parent
print('Menulis kode program menggunakan bahasa $programmingLanguage.');
}
}
Abstract Class dan Implicit Interfaces #
Dalam perancangan kode, kita sering kali memerlukan kelas yang bertindak sebagai kontrak standarisasi tanpa menyediakan implementasi detail.
Abstract Class #
Kelas yang ditandai dengan kata kunci abstract tidak dapat diinstansiasi secara langsung menggunakan kata kunci new. Kelas ini berguna untuk mendefinisikan metode abstrak (metode tanpa tubuh kode) yang wajib dilengkapi oleh seluruh subclass konkretnya.
abstract class Storage {
// Metode abstrak tanpa implementasi
Future<void> write(String key, String value);
Future<String?> read(String key);
}
// Subclass wajib mengimplementasikan metode abstrak di atas
class SecureStorage extends Storage {
@override
Future<void> write(String key, String value) async {
// Implementasi enkripsi data
}
@override
Future<String?> read(String key) async {
// Implementasi dekripsi data
return 'data';
}
}
Implicit Interfaces (Antarmuka Implisit) #
Satu karakteristik unik yang membedakan Dart dari Java atau C# adalah Dart tidak memiliki kata kunci interface khusus (kecuali modifikasi modern abstract interface class). Di Dart, setiap kelas secara implisit bertindak sebagai antarmuka.
Setiap kali sebuah kelas menggunakan kata kunci implements (bukan extends), kelas tersebut wajib mengimplementasikan ulang seluruh properti dan metode dari kelas target dari awal, tanpa mewarisi implementasi yang sudah ada.
class MockStorage implements Storage {
// Wajib menulis ulang implementasi meskipun parent memiliki concrete method
@override
Future<void> write(String key, String value) async => print('Mock Write');
@override
Future<String?> read(String key) async => 'mock_data';
}
Kapan Menggunakan extends vs implements? #
- Gunakan
extendsjika kita ingin membangun hubungan spesialisasi (is-a relation) dan ingin memanfaatkan pewarisan kode logika dari parent class. - Gunakan
implementsjika kita hanya ingin mengadopsi kontrak tipe data tanpa ingin menggunakan logika kode yang ada di dalamnya, sangat berguna saat membuat objek tiruan (mocking) untuk unit testing.
Mixins — Komposisi Kemampuan Tanpa Pewarisan #
Karena Dart hanya mendukung pewarisan tunggal, membagikan sekumpulan metode pembantu ke berbagai kelas yang berada di cabang hierarki yang berbeda menjadi tantangan tersendiri. Di sinilah Mixins masuk sebagai penyelamat.
Mixins adalah cara membagikan kode perilaku (behavior) lintas kelas tanpa menggunakan pewarisan kelas (inheritance). Kelas dapat menggunakan mixin menggunakan kata kunci with.
Perbedaan arsitektural antara pewarisan tunggal dan komposisi mixin dapat divisualisasikan pada diagram di bawah ini:
flowchart TD
subgraph SingleInheritance["Pewarisan Tunggal (extends)"]
direction TB
Base["Base Class: Kendaraan"] --> Sub["Sub-class: Mobil"]
Sub --> SubSub["Sub-class: MobilListrik"]
end
subgraph MixinComposition["Komposisi Mixin (with)"]
direction TB
Main["Class: UserService"]
M1["Mixin: Logging"]
M2["Mixin: Cacheable"]
M1 & M2 -.->|"Disisipkan (with)"| Main
endContoh Penerapan Mixin #
Mari kita buat mixin untuk menangani logging dan validasi data:
mixin Logger {
void logInfo(String message) {
print('[INFO - ${DateTime.now()}]: $message');
}
}
mixin Validator {
bool isValidEmail(String email) {
return email.contains('@');
}
}
// Menggunakan mixin lintas hierarki kelas tanpa pewarisan
class AuthService with Logger, Validator {
void register(String email) {
if (isValidEmail(email)) {
logInfo('Registrasi berhasil untuk $email');
} else {
print('Email tidak valid');
}
}
}
Membatasi Mixin dengan Kata Kunci on #
Kita dapat membatasi agar suatu mixin hanya diperbolehkan untuk disisipkan pada kelas yang mewarisi kelas tertentu menggunakan kata kunci on:
import 'package:flutter/material.dart';
// Mixin ini HANYA bisa digunakan oleh kelas yang merupakan turunan dari State (Flutter)
mixin LoadingStateMixin<T extends StatefulWidget> on State<T> {
bool isLoading = false;
void toggleLoading() {
setState(() {
isLoading = !isLoading;
});
}
}
Generics — Menulis Kode yang Fleksibel dan Type-Safe #
Generics adalah fitur yang memungkinkan kita menulis kelas, antarmuka, atau metode yang perilakunya dapat disesuaikan untuk berbagai macam tipe data, dengan tetap mempertahankan keamanan tipe (type safety) pada saat kompilasi.
Generics menggunakan parameter tipe yang diletakkan di dalam tanda kurung siku siku <T>:
// Membuat struktur data Stack generatif
class Stack<T> {
final List<T> _items = [];
void push(T value) => _items.add(value);
T pop() {
if (_items.isEmpty) throw StateError('Stack kosong');
return _items.removeLast();
}
}
void main() {
// Stack khusus untuk menampung Angka
final numberStack = Stack<int>();
numberStack.push(10);
// numberStack.push('Teks'); // ERROR compile-time! Mencegah bug sejak awal
// Stack khusus untuk menampung Teks
final textStack = Stack<String>();
textStack.push('Hello');
}
Batasan Tipe (Bounded Generics) #
Kita bisa membatasi tipe data apa saja yang diperbolehkan masuk ke parameter generics menggunakan kata kunci extends:
// Hanya memperbolehkan tipe num (int/double) ke dalam kotak ini
class CalculatorBox<T extends num> {
final T value;
CalculatorBox(this.value);
double get half => value / 2;
}
Operator Overloading dan Extension Methods #
Dart memberikan dua fitur pemanis sintaksis tambahan untuk membuat kode kita terasa lebih alami dan menyatu dengan fitur bawaan bahasa:
1. Operator Overloading #
Kita dapat mendefinisikan ulang perilaku dari operator matematika atau pembanding (+, -, ==, [], dll.) untuk kelas buatan kita sendiri:
class Vector2D {
final double x;
final double y;
const Vector2D(this.x, this.y);
// Mendefinisikan operator tambah (+)
Vector2D operator +(Vector2D other) {
return Vector2D(x + other.x, y + other.y);
}
// Mendefinisikan operator pembanding kesamaan (==)
@override
bool operator ==(Object other) =>
other is Vector2D && x == other.x && y == other.y;
@override
int get hashCode => Object.hash(x, y);
@override
String toString() => 'Vector2D($x, $y)';
}
void main() {
final v1 = Vector2D(2.0, 3.0);
final v2 = Vector2D(4.0, 1.0);
// Menggunakan operator + kustom secara natural
final v3 = v1 + v2;
print(v3); // Output: Vector2D(6.0, 4.0)
}
2. Extension Methods #
Extension methods memungkinkan kita menambahkan fungsi atau metode baru ke dalam kelas yang sudah ada — bahkan kelas bawaan milik SDK Dart atau pihak ketiga yang kodenya tidak bisa kita modifikasi secara langsung:
// Menambahkan kemampuan kapitalisasi pada tipe data bawaan String
extension StringCapitalizeExtension on String {
String toCapitalized() {
if (isEmpty) return this;
return '${this[0].toUpperCase()}${substring(1)}';
}
}
void main() {
final myText = 'belajar flutter';
// Memanggil extension method baru
print(myText.toCapitalized()); // Output: Belajar flutter
}
Ringkasan #
- Enkapsulasi: Gunakan awalan garis bawah (
_) untuk membatasi akses variabel di tingkat pustaka (library-level privacy) dan kelola akses baca-tulis menggunakan Getter dan Setter.- Konstruktor: Dart menyediakan 5 konstruktor kustom. Selalu prioritaskan
const constructoruntuk komponen widget di Flutter untuk meminimalkan beban rendering layar.- Pewarisan (Inheritance): Gunakan pewarisan tunggal via
extendsuntuk berbagi logika konkret dari kelas induk secara vertikal.- Abstract Class & Interface: Kelas abstrak mendefinisikan kontrak sebagian, sedangkan seluruh kelas di Dart bertindak sebagai interface implisit saat digunakan dengan kata kunci
implements.- Mixins: Solusi komposisi kode horizontal menggunakan
withuntuk membagikan kemampuan lintas hierarki tanpa menghadapi masalah Diamond Problem.- Generics: Menghadirkan penulisan kode komponen yang fleksibel dan dapat digunakan kembali namun tetap aman dari kesalahan tipe data (type safety).
- Extension & Overloading: Memberikan fleksibilitas penambahan fungsi pada pustaka siap pakai (Extensions) dan mengizinkan modifikasi operator bawaan (Overloading).
← Sebelumnya: Collections Berikutnya: Functional Programming →