OOP di Dart #
Dart adalah bahasa berorientasi objek penuh — semua nilai adalah objek, semua objek adalah instance dari class, dan semua class (kecuali Null) merupakan turunan dari Object. Memahami OOP di Dart adalah fondasi untuk menulis kode Flutter yang terstruktur, dapat diuji, dan mudah dipelihara.
Class dan Instance #
Class adalah blueprint dari sebuah objek. Instance adalah realisasi konkret dari blueprint tersebut.
class Produk {
// Instance variables (properties)
final String id;
final String nama;
double harga;
bool _tersedia = true; // private (diawali underscore)
// Constructor
Produk({
required this.id,
required this.nama,
required this.harga,
});
// Getter
bool get tersedia => _tersedia;
String get label => '$nama (Rp ${harga.toStringAsFixed(0)})';
// Setter
set harga(double nilai) {
if (nilai < 0) throw ArgumentError('Harga tidak boleh negatif');
harga = nilai;
}
// Instance method
void nonaktifkan() {
_tersedia = false;
}
// Override toString untuk representasi yang lebih baik
@override
String toString() => 'Produk($id, $nama, $harga)';
}
// Membuat instance
final produk = Produk(id: 'P001', nama: 'Apel', harga: 5000);
print(produk.label); // Apel (Rp 5000)
print(produk.tersedia); // true
produk.nonaktifkan();
print(produk.tersedia); // false
Jenis-jenis Constructor #
Dart menyediakan beberapa cara untuk mendefinisikan constructor sesuai kebutuhan:
class Point {
final double x;
final double y;
// 1. Generative constructor (paling umum)
Point(this.x, this.y);
// 2. Named constructor -- beberapa cara membuat instance
Point.origin() : x = 0, y = 0;
Point.fromJson(Map<String, dynamic> json)
: x = json['x'] as double,
y = json['y'] as double;
// 3. Const constructor -- instance compile-time constant
const Point.zero() : x = 0, y = 0;
// 4. Factory constructor -- kontrol penuh atas pembuatan instance
factory Point.fromString(String str) {
final parts = str.split(',');
return Point(double.parse(parts[0]), double.parse(parts[1]));
}
// 5. Redirecting constructor -- delegasikan ke constructor lain
Point.unit() : this(1, 0);
@override
String toString() => 'Point($x, $y)';
}
void main() {
print(Point(3, 4)); // Point(3.0, 4.0)
print(Point.origin()); // Point(0.0, 0.0)
print(Point.fromString('5,6')); // Point(5.0, 6.0)
print(Point.unit()); // Point(1.0, 0.0)
const p1 = Point.zero();
const p2 = Point.zero();
print(identical(p1, p2)); // true -- instance yang sama!
}
Initializer List #
Initializer list dieksekusi sebelum body constructor — berguna untuk validasi atau inisialisasi final field:
class RectangleValidated {
final double lebar;
final double tinggi;
// Initializer list: validasi sebelum body dieksekusi
RectangleValidated(this.lebar, this.tinggi)
: assert(lebar > 0, 'Lebar harus > 0'),
assert(tinggi > 0, 'Tinggi harus > 0');
double get luas => lebar * tinggi;
}
Inheritance — extends #
Inheritance memungkinkan sebuah class mewarisi properties dan methods dari class lain. Dart hanya mendukung single inheritance — sebuah class hanya bisa meng-extend satu class.
// Base class
class Kendaraan {
final String merk;
int kecepatanMaks;
Kendaraan({required this.merk, required this.kecepatanMaks});
void bergerak() {
print('$merk bergerak');
}
@override
String toString() => '$merk (max: ${kecepatanMaks}km/h)';
}
// Subclass
class Mobil extends Kendaraan {
final int jumlahPintu;
Mobil({
required super.merk, // super parameter (Dart 2.17+)
required super.kecepatanMaks,
required this.jumlahPintu,
});
@override
void bergerak() {
super.bergerak(); // panggil method parent
print('dengan $jumlahPintu pintu');
}
void klakson() => print('$merk: Beeep!');
}
class Truk extends Kendaraan {
final double kapasitasMuatan; // ton
Truk({
required super.merk,
required super.kecepatanMaks,
required this.kapasitasMuatan,
});
@override
void bergerak() {
print('$merk (truk $kapasitasMuatan ton) bergerak perlahan');
}
}
void main() {
final mobil = Mobil(merk: 'Toyota', kecepatanMaks: 180, jumlahPintu: 4);
final truk = Truk(merk: 'Mercedes', kecepatanMaks: 120, kapasitasMuatan: 10);
// Polymorphism -- sama-sama Kendaraan, perilaku berbeda
final List<Kendaraan> kendaraan = [mobil, truk];
for (final k in kendaraan) {
k.bergerak();
}
}
Abstract Class — Kontrak Parsial #
Abstract class mendefinisikan kontrak yang harus diimplementasikan subclass, tapi boleh juga menyertakan implementasi konkret. Abstract class tidak bisa di-instantiate langsung.
abstract class Repository<T> {
// Abstract methods -- wajib diimplementasikan subclass
Future<T?> getById(String id);
Future<List<T>> getAll();
Future<void> save(T entity);
Future<void> delete(String id);
// Concrete method -- sudah ada implementasinya
Future<bool> exists(String id) async {
return await getById(id) != null;
}
// Template method pattern -- algoritma di base class,
// detail di subclass
Future<T> getOrThrow(String id) async {
final entity = await getById(id);
if (entity == null) {
throw NotFoundException('Entity dengan id $id tidak ditemukan');
}
return entity;
}
}
// Implementasi konkret
class UserRepository extends Repository<User> {
final Database _db;
UserRepository(this._db);
@override
Future<User?> getById(String id) async {
final row = await _db.query('SELECT * FROM users WHERE id = ?', [id]);
return row.isEmpty ? null : User.fromRow(row.first);
}
@override
Future<List<User>> getAll() async {
final rows = await _db.query('SELECT * FROM users');
return rows.map(User.fromRow).toList();
}
@override
Future<void> save(User user) async {
await _db.execute(
'INSERT OR REPLACE INTO users VALUES (?, ?, ?)',
[user.id, user.nama, user.email],
);
}
@override
Future<void> delete(String id) async {
await _db.execute('DELETE FROM users WHERE id = ?', [id]);
}
}
Interface — implements #
Di Dart, setiap class secara implisit mendefinisikan interface. Kamu bisa menggunakan implements untuk mengimplementasikan interface tanpa mewarisi implementasinya. Semua method harus diimplementasikan ulang.
// Interface (implicit -- setiap class adalah interface)
abstract interface class Serializable {
Map<String, dynamic> toJson();
String toJsonString() => jsonEncode(toJson()); // bisa ada default impl
}
abstract interface class Comparable<T> {
int compareTo(T other);
bool operator <(T other) => compareTo(other) < 0;
bool operator >(T other) => compareTo(other) > 0;
}
// Class mengimplementasikan beberapa interface sekaligus
class Produk implements Serializable, Comparable<Produk> {
final String id;
final String nama;
final double harga;
Produk({required this.id, required this.nama, required this.harga});
// Wajib diimplementasikan karena Serializable
@override
Map<String, dynamic> toJson() => {
'id': id,
'nama': nama,
'harga': harga,
};
// Wajib diimplementasikan karena Comparable<Produk>
@override
int compareTo(Produk other) => harga.compareTo(other.harga);
}
extends vs implements — Kapan Mana? #
extends (inheritance):
✓ Relasi "is-a" yang kuat
✓ Ingin mewarisi IMPLEMENTASI dari parent
✓ Ingin override sebagian method saja
✗ Hanya bisa extend SATU class
implements (interface):
✓ Ingin mendefinisikan KONTRAK tanpa mewarisi implementasi
✓ Ingin implement beberapa interface sekaligus
✓ Cocok untuk mock di testing
✗ Harus mengimplementasikan SEMUA method
Mixin — Komposisi Tanpa Inheritance #
Mixin adalah cara menambahkan kemampuan ke sebuah class tanpa menggunakan inheritance. Ini menyelesaikan masalah diamond problem yang muncul dalam multiple inheritance.
// Mixin: hanya berisi method dan field, tidak bisa di-instantiate
mixin Logging {
void log(String pesan) {
print('[${DateTime.now()}] ${runtimeType}: $pesan');
}
void logError(String pesan, [Object? error]) {
print('[ERROR] ${runtimeType}: $pesan${error != null ? ' -- $error' : ''}');
}
}
mixin Cacheable {
final Map<String, dynamic> _cache = {};
T? getFromCache<T>(String key) => _cache[key] as T?;
void saveToCache(String key, dynamic value) {
_cache[key] = value;
}
void clearCache() => _cache.clear();
}
mixin Validatable {
// Abstract member -- class yang pakai mixin ini harus punya validasi()
Map<String, String?> validasi();
bool get isValid => validasi().values.every((v) => v == null);
List<String> get errorMessages =>
validasi().values.whereType<String>().toList();
}
// Menggunakan beberapa mixin sekaligus
class UserService extends BaseService
with Logging, Cacheable {
Future<User?> getUser(String id) async {
// Dari mixin Cacheable
final cached = getFromCache<User>(id);
if (cached != null) {
log('Cache hit untuk user $id'); // dari mixin Logging
return cached;
}
log('Fetching user $id dari API');
final user = await api.getUser(id);
if (user != null) saveToCache(id, user);
return user;
}
}
Mixin dengan on Clause #
// Mixin yang hanya bisa dipakai oleh class tertentu
mixin AnimationMixin on State<StatefulWidget> {
late AnimationController controller;
@override
void initState() {
super.initState();
// Bisa mengakses State karena on State<StatefulWidget>
controller = AnimationController(
vsync: this as TickerProvider,
duration: const Duration(milliseconds: 300),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
// Hanya bisa dipakai oleh subclass State
class MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin, AnimationMixin {
// AnimationMixin sudah mengurus initState dan dispose
}
Generics — Tipe yang Fleksibel dan Type-Safe #
Generics memungkinkan kamu menulis kode yang bekerja untuk berbagai tipe sambil tetap mempertahankan type safety:
// Generic class
class Stack<T> {
final List<T> _items = [];
void push(T item) => _items.add(item);
T pop() {
if (_items.isEmpty) throw StateError('Stack kosong');
return _items.removeLast();
}
T get peek => _items.last;
bool get isEmpty => _items.isEmpty;
int get length => _items.length;
}
// Generic function
T maxOf<T extends Comparable<T>>(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// Bounded generics -- batasi tipe yang diizinkan
class NumberBox<T extends num> {
T value;
NumberBox(this.value);
NumberBox<T> operator +(NumberBox<T> other) {
return NumberBox(value + other.value as T);
}
}
void main() {
// Type-safe -- compiler tahu tipe elemen
final intStack = Stack<int>();
intStack.push(1);
intStack.push(2);
print(intStack.pop()); // 2
final stringStack = Stack<String>();
stringStack.push('hello');
// stringStack.push(42); // ERROR compile time!
print(maxOf(10, 20)); // 20
print(maxOf('apple', 'mango')); // mango
}
Operator Overloading #
Dart memungkinkan kamu mendefinisikan perilaku operator (+, -, *, ==, [], dll.) untuk class buatanmu:
class Vector2D {
final double x;
final double y;
const Vector2D(this.x, this.y);
// Operator aritmatika
Vector2D operator +(Vector2D other) => Vector2D(x + other.x, y + other.y);
Vector2D operator -(Vector2D other) => Vector2D(x - other.x, y - other.y);
Vector2D operator *(double scalar) => Vector2D(x * scalar, y * scalar);
Vector2D operator -() => Vector2D(-x, -y); // unary minus
// Equality
@override
bool operator ==(Object other) =>
other is Vector2D && x == other.x && y == other.y;
@override
int get hashCode => Object.hash(x, y);
// Indexing -- akses dengan [0] dan [1]
double operator [](int index) {
return switch (index) {
0 => x,
1 => y,
_ => throw RangeError.index(index, this, 'index', null, 2),
};
}
double get magnitude => (x * x + y * y).sqrt();
@override
String toString() => 'Vector2D($x, $y)';
}
void main() {
final v1 = Vector2D(1, 2);
final v2 = Vector2D(3, 4);
print(v1 + v2); // Vector2D(4.0, 6.0)
print(v1 * 3); // Vector2D(3.0, 6.0)
print(v1 == Vector2D(1, 2)); // true
print(v1[0]); // 1.0
}
Extension Methods — Tambahkan Method ke Tipe yang Ada #
Extension methods memungkinkan menambahkan method ke class yang sudah ada — termasuk class bawaan Dart — tanpa mengubah atau membuat subclass-nya:
extension StringExtension on String {
// Capitalize first letter
String get capitalized =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
// Title case
String get titleCase =>
split(' ').map((w) => w.capitalized).join(' ');
// Validasi email sederhana
bool get isValidEmail =>
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
// Parse ke int dengan default
int toIntOrDefault(int defaultValue) =>
int.tryParse(this) ?? defaultValue;
}
extension ListExtension<T> on List<T> {
// Chunk list menjadi sub-list dengan ukuran tertentu
List<List<T>> chunked(int size) {
final result = <List<T>>[];
for (var i = 0; i < length; i += size) {
result.add(sublist(i, (i + size).clamp(0, length)));
}
return result;
}
// Unique berdasarkan key
List<T> uniqueBy<K>(K Function(T) key) {
final seen = <K>{};
return where((item) => seen.add(key(item))).toList();
}
}
void main() {
print('flutter developer'.titleCase); // Flutter Developer
print('[email protected]'.isValidEmail); // true
print('abc'.toIntOrDefault(0)); // 0
final angka = [1, 2, 3, 4, 5, 6, 7];
print(angka.chunked(3)); // [[1, 2, 3], [4, 5, 6], [7]]
final users = [
User(id: '1', nama: 'A'),
User(id: '2', nama: 'B'),
User(id: '1', nama: 'A duplikat'),
];
print(users.uniqueBy((u) => u.id).length); // 2
}
Ringkasan #
- Dart mendukung OOP penuh: class, inheritance (
extends), interface (implements), dan mixin (with) — masing-masing untuk kasus yang berbeda.extendsuntuk relasi “is-a” dan mewarisi implementasi.implementsuntuk mendefinisikan kontrak tanpa mewarisi implementasi.withuntuk menambahkan kemampuan lintas hierarki tanpa inheritance.- Constructor di Dart hadir dalam 5 bentuk: generative, named, const, factory, dan redirecting — masing-masing untuk skenario berbeda.
- Abstract class mendefinisikan kontrak parsial — boleh punya implementasi konkret dan abstract method yang wajib diimplementasikan subclass.
- Mixin adalah solusi Dart untuk code reuse lintas hierarki tanpa diamond problem. Gunakan
onclause untuk membatasi mixin hanya pada supertype tertentu.- Generics memungkinkan kode yang reusable dan type-safe. Gunakan
extendspada type parameter untuk membatasi tipe yang diizinkan.- Operator overloading memungkinkan class kustom menggunakan sintaks operator bawaan (+, -, ==, [], dll.).
- Extension methods menambahkan method ke tipe yang sudah ada tanpa subclassing — sangat berguna untuk menambahkan helper ke
String,List, atau class Flutter.
← Sebelumnya: Collections Berikutnya: Functional Programming →