Konstanta #
Konstanta bukan sekadar variabel yang kebetulan tidak pernah diubah — ia adalah pernyataan eksplisit kepada compiler, tim, dan pembaca kode bahwa nilai ini tidak boleh berubah, by design. Dart menyediakan dua kata kunci untuk tujuan ini: const dan final. Keduanya mencegah perubahan nilai, tapi mekanisme dan implikasinya sangat berbeda. Memilih yang salah tidak akan langsung menyebabkan bug, tapi memilih yang tepat membuka optimasi compiler, mencegah error logis yang halus, dan — khususnya di Flutter — berdampak nyata pada performa rendering. Artikel ini membahas keduanya secara mendalam, beserta pola pengelolaan konstanta yang baik dalam proyek nyata.
Masalah yang Dipecahkan Konstanta #
Bayangkan sebuah aplikasi e-commerce dengan logika diskon yang tersebar di puluhan file:
// Di file checkout.dart
double diskon = harga * 0.1;
// Di file keranjang.dart
double potongan = subtotal * 0.1;
// Di file laporan.dart
double penguranganHarga = totalBelanja * 0.10;
Tiga bulan kemudian, kebijakan bisnis berubah: diskon naik dari 10% ke 15%. Developer harus mencari dan mengganti semua angka 0.1 dan 0.10 di seluruh codebase — dengan risiko melewatkan satu dan menyebabkan inkonsistensi. Angka seperti ini disebut magic number dan ini adalah masalah yang dipecahkan konstanta.
// ANTI-PATTERN: magic number tersebar di seluruh codebase
double diskon = harga * 0.1;
double pajakPpn = subtotal * 0.11;
int batasRetry = 3;
int timeoutDetik = 30;
// BENAR: definisikan sekali, gunakan di mana saja
const double persentaseDiskon = 0.10;
const double tarifPpn = 0.11;
const int maksRetry = 3;
const int timeoutKoneksi = 30;
double diskon = harga * persentaseDiskon;
double pajak = subtotal * tarifPpn;
Selain menghilangkan magic number, konstanta juga memberikan manfaat lain: compiler bisa memvalidasi penggunaannya, refactoring menjadi satu titik perubahan, dan kode menjadi self-documenting karena nama konstanta menjelaskan makna nilainya.
const — Konstanta Compile-Time
#
const mendeklarasikan nilai yang sudah sepenuhnya diketahui saat kompilasi — sebelum program dijalankan. Compiler mengevaluasi ekspresi const dan menyematkan hasilnya langsung ke dalam kode yang dikompilasi, seperti mengganti nama variabel dengan nilainya secara literal.
// Nilai primitif — paling umum
const double phi = 3.14159265358979;
const int maksKarakter = 280; // batas karakter tweet
const String versiApi = 'v2';
const bool modeProduksi = true;
// Ekspresi compile-time — semua komponen harus const atau literal
const double duaPhi = 2 * phi; // ✓ phi sudah const
const int batasKarakterJudul = maksKarakter ~/ 2; // ✓ maksKarakter sudah const
const String urlBase = 'https://api.example.com/' + versiApi; // ✓
// Koleksi const — benar-benar immutable hingga ke isinya
const List<String> metodePembayaran = ['transfer', 'kartu_kredit', 'qris'];
const Map<String, int> kodePrioritas = {'rendah': 1, 'sedang': 2, 'tinggi': 3};
const Set<String> formatGambarDidukung = {'jpg', 'png', 'webp'};
Nilai yang bergantung pada runtime tidak bisa dijadikan const — compiler akan langsung menolaknya:
// Semua ini error kompilasi
const DateTime sekarang = DateTime.now(); // ✗ bergantung pada waktu runtime
const String inputPengguna = stdin.readLineSync()!; // ✗ bergantung pada input
const int nilaiAcak = Random().nextInt(100); // ✗ bergantung pada RNG runtime
// Solusi: gunakan final untuk nilai runtime yang tidak boleh diubah
final DateTime waktuMulai = DateTime.now(); // ✓
final String konfigurasi = loadConfig(); // ✓
Apa yang Terjadi di Balik Layar #
Ketika compiler menemukan const double phi = 3.14159265358979, ia tidak mengalokasikan memori baru setiap kali phi digunakan — ia langsung menyematkan nilai 3.14159265358979 ke dalam bytecode. Inilah yang membuat const berbeda secara fundamental dari final:
flowchart LR
subgraph "Compile Time"
C1["const phi = 3.14159"] --> C2["Compiler evaluasi"]
C2 --> C3["3.14159 disematkan\nke bytecode"]
end
subgraph "Runtime"
R1["final waktu = DateTime.now()"] --> R2["Program jalan"]
R2 --> R3["Alokasi memori\nNilai dievaluasi"]
end
const Constructor — Objek Konstan
#
const tidak terbatas pada nilai primitif. Kelas bisa mendukung const constructor, yang memungkinkan pembuatan instance yang sepenuhnya immutable dan dievaluasi saat kompilasi.
Syarat untuk mendefinisikan const constructor:
- Semua properti kelas harus
final - Body constructor harus kosong (tidak ada logika runtime)
- Semua nilai yang diteruskan ke constructor harus bisa menjadi
const
class Titik {
final double x;
final double y;
// const constructor — body harus kosong
const Titik(this.x, this.y);
// Method boleh ada — tidak mengubah state, hanya menghitung
double jarakKe(Titik lain) {
final dx = x - lain.x;
final dy = y - lain.y;
return (dx * dx + dy * dy); // jarak kuadrat, tanpa sqrt untuk performa
}
@override
String toString() => 'Titik($x, $y)';
}
class Warna {
final int r;
final int g;
final int b;
final double opacity;
const Warna(this.r, this.g, this.b, {this.opacity = 1.0});
// Named constructor juga bisa const
const Warna.merah() : r = 255, g = 0, b = 0, opacity = 1.0;
const Warna.hijau() : r = 0, g = 255, b = 0, opacity = 1.0;
const Warna.biru() : r = 0, g = 0, b = 255, opacity = 1.0;
const Warna.transparan() : r = 0, g = 0, b = 0, opacity = 0.0;
}
void main() {
const pusat = Titik(0, 0); // ✓ instance const
const ujung = Titik(3, 4); // ✓ instance const
final titikRuntime = Titik(1.5, 2.5); // ✓ juga valid, tapi bukan const
const merah = Warna.merah(); // ✓
const biru = Warna(0, 0, 255); // ✓
}
// ANTI-PATTERN: kelas yang bisa const tapi tidak mendefinisikan const constructor
class KonfigurasiWarna {
final int r;
final int g;
final int b;
KonfigurasiWarna(this.r, this.g, this.b); // ✗ tidak const — peluang const terlewatkan
}
// BENAR: tambahkan const constructor jika semua properti final dan tidak ada logika
class KonfigurasiWarna {
final int r;
final int g;
final int b;
const KonfigurasiWarna(this.r, this.g, this.b); // ✓ mendukung const
}
Canonical Instances — Satu Objek untuk Nilai yang Sama #
Fitur paling unik dari const constructor adalah canonical instances: dua ekspresi const dengan nilai yang identik dijamin mengacu ke objek yang sama persis di memori, bukan dua objek berbeda yang kebetulan nilainya sama.
const a = Titik(0, 0);
const b = Titik(0, 0);
const c = Titik(1, 1);
print(identical(a, b)); // true — satu objek yang sama di memori
print(identical(a, c)); // false — dua objek berbeda
// Bandingkan dengan objek non-const:
final x = Titik(0, 0);
final y = Titik(0, 0);
print(identical(x, y)); // false — dua objek berbeda meski nilainya sama
Ini bukan hanya detail teknis — ini memiliki implikasi besar di Flutter. Widget const dengan nilai yang sama tidak perlu dibuat ulang saat rebuild karena Dart menjamin instance-nya identik:
// Di Flutter — const widget tidak di-rebuild saat parent rebuild
Widget build(BuildContext context) {
return Column(
children: [
const Text('Label tetap'), // ✓ tidak di-rebuild
const Icon(Icons.star, size: 24), // ✓ tidak di-rebuild
Text(nilaiDariState), // ini yang berubah saat state update
],
);
}
final — Konstanta Runtime
#
final mendeklarasikan variabel yang nilainya hanya bisa di-set satu kali, tapi nilainya bisa ditentukan kapan saja — saat deklarasi, di constructor, atau di momen pertama dibutuhkan. Ini adalah kata kunci yang lebih fleksibel dibanding const karena bisa menampung nilai yang baru diketahui saat program berjalan.
// Nilai runtime yang tidak boleh berubah setelah di-set
final DateTime waktuMulaiApp = DateTime.now();
final String idSesi = generateUuid();
final Map<String, dynamic> konfigurasi = loadConfigFromFile('config.json');
// Nilai yang bisa dikomputasi dari runtime
final int totalHalaman = (jumlahData / itemPerHalaman).ceil();
final String pesanSambutan = 'Selamat datang, ${pengguna.nama}!';
final di Properti Kelas
#
final paling sering ditemui sebagai properti kelas yang diinisialisasi melalui constructor — pola ini adalah fondasi dari desain immutable data class di Dart:
class Pengguna {
final String id;
final String nama;
final String email;
final DateTime tanggalDaftar;
// Semua properti final diinisialisasi di constructor
const Pengguna({
required this.id,
required this.nama,
required this.email,
required this.tanggalDaftar,
});
// Karena immutable, "perubahan" dilakukan dengan membuat objek baru
Pengguna salinDengan({String? nama, String? email}) {
return Pengguna(
id: id,
nama: nama ?? this.nama,
email: email ?? this.email,
tanggalDaftar: tanggalDaftar,
);
}
}
void main() {
final pengguna = Pengguna(
id: 'U001',
nama: 'Budi Santoso',
email: '[email protected]',
tanggalDaftar: DateTime(2024, 1, 15),
);
// pengguna.nama = 'Siti'; // ✗ error: properti final tidak bisa diubah
// Buat objek baru dengan nilai yang diperbarui
final penggunaDiperbarui = pengguna.salinDengan(nama: 'Budi S.');
print(penggunaDiperbarui.nama); // Budi S.
print(pengguna.nama); // Budi Santoso — objek asli tidak berubah
}
final di Initializer List
#
Constructor Dart memiliki fitur initializer list — blok sebelum body constructor yang bisa menginisialisasi properti final dengan ekspresi yang lebih kompleks:
class Lingkaran {
final double jariJari;
final double luas;
final double keliling;
// Initializer list menghitung luas dan keliling dari jariJari
Lingkaran(this.jariJari)
: luas = 3.14159 * jariJari * jariJari,
keliling = 2 * 3.14159 * jariJari;
}
void main() {
final l = Lingkaran(5);
print(l.luas); // 78.53975
print(l.keliling); // 31.4159
// l.luas = 100; // ✗ error: final
}
Perbedaan const dan final secara Menyeluruh
#
Pemahaman yang tepat tentang perbedaan keduanya adalah kunci untuk memilih yang benar di setiap situasi:
| Aspek | const |
final |
|---|---|---|
| Waktu evaluasi | Compile time | Runtime (saat di-set pertama kali) |
| Nilai runtime | ✗ tidak bisa | ✓ bisa |
| Isi koleksi | Immutable sepenuhnya | Mutable (List bisa .add()) |
| Canonical instance | ✓ objek sama di memori | ✗ objek baru setiap kali |
| Bisa di properti kelas | ✓ (harus static) | ✓ (instance maupun static) |
Perlu static di kelas |
✓ untuk instance prop | ✗ tidak perlu |
| Optimasi compiler | Maksimal | Sebagian |
// Ilustrasi perbedaan isi koleksi
const List<int> listConst = [1, 2, 3];
final List<int> listFinal = [1, 2, 3];
listConst.add(4); // ✗ error runtime: Unsupported operation: add
listFinal.add(4); // ✓ berhasil — referensi final, isi mutable
print(listFinal); // [1, 2, 3, 4]
flowchart TD
A{Nilai sudah diketahui\nsaat compile time?} -- Ya --> B{Semua komponen\nbisa const?}
B -- Ya --> C[Gunakan const]
B -- Tidak --> D[Gunakan final]
A -- Tidak --> E{Nilai perlu\nberubah setelah di-set?}
E -- Ya --> F[Gunakan var atau\ntipe eksplisit]
E -- Tidak --> D
Mengorganisasi Konstanta dalam Proyek #
Proyek kecil mungkin cukup dengan mendefinisikan konstanta di file yang memakainya. Tapi ketika proyek berkembang, konstanta yang tersebar di banyak file menjadi sulit dikelola. Ada beberapa pola yang umum digunakan.
Pola 1: Kelas Abstrak Berisi Konstanta #
Pendekatan paling umum di Dart — mengumpulkan konstanta tematik dalam kelas abstrak sehingga tidak bisa diinstansiasi:
// lib/core/constants/app_constants.dart
abstract class AppConstants {
// Cegah instansiasi
AppConstants._();
// Konfigurasi jaringan
static const String urlBase = 'https://api.example.com';
static const String versiApi = 'v2';
static const int timeoutDetik = 30;
static const int maksRetry = 3;
// Batas UI
static const int maksKarakterJudul = 100;
static const int maksKarakterDeskripsi = 500;
static const int itemPerHalaman = 20;
// Durasi animasi
static const Duration durasiAnimasiPendek = Duration(milliseconds: 150);
static const Duration durasiAnimasiNormal = Duration(milliseconds: 300);
static const Duration durasiAnimasiLambat = Duration(milliseconds: 500);
}
// Penggunaan
final url = '${AppConstants.urlBase}/${AppConstants.versiApi}/produk';
Pola 2: Konstanta per Fitur #
Untuk proyek besar dengan arsitektur berbasis fitur, setiap modul/fitur bisa memiliki file konstanta sendiri:
// lib/features/auth/constants/auth_constants.dart
abstract class AuthConstants {
AuthConstants._();
static const int minPanjangPassword = 8;
static const int maksPanjangPassword = 72;
static const int durasiTokenMenit = 60;
static const int durasiRefreshTokenHari = 30;
static const int maksGagalLogin = 5;
static const Duration jedaSetelahGagal = Duration(minutes: 15);
}
// lib/features/produk/constants/produk_constants.dart
abstract class ProdukConstants {
ProdukConstants._();
static const int maksGambarPerProduk = 10;
static const double ukuranMaksGambarMb = 5.0;
static const List<String> formatGambarDidukung = ['jpg', 'jpeg', 'png', 'webp'];
static const int minStokPeringatan = 5;
}
Pola 3: Top-Level Constants di File Terpisah #
Alternatif yang lebih sederhana — konstanta sebagai variabel top-level biasa, tanpa kelas pembungkus:
// lib/core/constants.dart
// Jaringan
const String kUrlBase = 'https://api.example.com';
const int kTimeoutDetik = 30;
// UI
const double kBorderRadius = 8.0;
const double kPadding = 16.0;
const double kPaddingKecil = 8.0;
// Warna (jika tidak menggunakan ThemeData)
const int kWarnaUtamaHex = 0xFF6200EE;
const int kWarnaSekunderHex = 0xFF03DAC6;
Prefix k adalah konvensi yang digunakan oleh Flutter SDK sendiri untuk membedakan konstanta dari variabel biasa.
// ANTI-PATTERN: mendefinisikan konstanta yang sama di banyak tempat
// Di file_a.dart:
const int timeoutDetik = 30;
// Di file_b.dart:
const int networkTimeout = 30; // duplikasi — satu angka, dua nama berbeda
// Di file_c.dart:
const int koneksiTimeout = 30; // duplikasi lagi
// BENAR: satu definisi di satu tempat, digunakan di mana saja
// Di app_constants.dart:
static const int timeoutKoneksi = 30;
// Di file_a.dart, file_b.dart, file_c.dart:
import 'app_constants.dart';
// Gunakan AppConstants.timeoutKoneksi
Konstanta di Enum #
Enum di Dart adalah cara yang lebih terstruktur untuk mendefinisikan sekumpulan nilai konstanta yang terkait. Dart 2.17 ke atas mendukung enhanced enum yang bisa memiliki properti dan method:
// Enum sederhana
enum StatusPesanan { menunggu, diproses, dikirim, selesai, dibatalkan }
// Enhanced enum — bisa memiliki properti, constructor, dan method
enum MetodePembayaran {
transfer(kode: 'TF', labelTampilan: 'Transfer Bank', biayaAdmin: 2500),
kartuKredit(kode: 'CC', labelTampilan: 'Kartu Kredit', biayaAdmin: 0),
qris(kode: 'QR', labelTampilan: 'QRIS', biayaAdmin: 0),
tunai(kode: 'CS', labelTampilan: 'Bayar di Tempat', biayaAdmin: 0);
final String kode;
final String labelTampilan;
final int biayaAdmin;
const MetodePembayaran({
required this.kode,
required this.labelTampilan,
required this.biayaAdmin,
});
bool get gratis => biayaAdmin == 0;
}
void main() {
final metode = MetodePembayaran.transfer;
print(metode.labelTampilan); // Transfer Bank
print(metode.biayaAdmin); // 2500
print(metode.gratis); // false
// Iterasi semua nilai enum
for (final m in MetodePembayaran.values) {
print('${m.labelTampilan}: ${m.gratis ? "gratis" : "Rp${m.biayaAdmin}"}');
}
}
// ANTI-PATTERN: menggunakan String atau int sebagai pengganti enum
const String statusMenunggu = 'WAITING';
const String statusDiproses = 'PROCESSING';
// Tidak ada jaminan compiler bahwa hanya nilai ini yang valid
void prosesOrder(String status) {
if (status == 'WAITNG') { ... } // typo — compiler tidak mendeteksi
}
// BENAR: gunakan enum untuk sekumpulan nilai tetap yang terkait
enum StatusPesanan { menunggu, diproses, dikirim, selesai, dibatalkan }
void prosesOrder(StatusPesanan status) {
if (status == StatusPesanan.menunggu) { ... } // ✓ compiler memvalidasi
}
Implikasi Performa — Terutama di Flutter #
Pemilihan const vs final vs variabel biasa memiliki dampak nyata pada performa aplikasi Flutter. Ini bukan optimasi prematur — ini adalah kebiasaan yang perlu dibangun sejak awal.
Widget const Tidak Di-Rebuild
#
Flutter me-rebuild widget tree setiap kali state berubah. Widget yang tidak berubah seharusnya tidak ikut di-rebuild — dan cara memberitahu Flutter bahwa sebuah widget tidak akan berubah adalah dengan menggunakan const:
// ANTI-PATTERN: widget statis tanpa const — di-rebuild setiap kali parent rebuild
class HalamanUtama extends StatefulWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Toko Online'), // ✗ di-rebuild setiap kali
actions: [
Icon(Icons.shopping_cart), // ✗ di-rebuild setiap kali
SizedBox(width: 16), // ✗ di-rebuild setiap kali
],
),
body: Column(
children: [
Text('Selamat Datang'), // ✗ di-rebuild setiap kali
_kontenDinamis(), // ini yang memang perlu rebuild
],
),
);
}
}
// BENAR: widget statis menggunakan const — tidak di-rebuild
class HalamanUtama extends StatefulWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Toko Online'), // ✓ tidak di-rebuild
actions: const [
Icon(Icons.shopping_cart), // ✓ tidak di-rebuild
SizedBox(width: 16), // ✓ tidak di-rebuild
],
),
body: Column(
children: [
const Text('Selamat Datang'), // ✓ tidak di-rebuild
_kontenDinamis(), // ini yang memang perlu rebuild
],
),
);
}
}
Canonical Instance Menghemat Alokasi Memori #
Karena const menghasilkan canonical instance, memanggil constructor yang sama dengan nilai yang sama berkali-kali tidak mengalokasikan objek baru:
// Tanpa const — setiap panggilan membuat objek baru di heap
var warna1 = Color(0xFF6200EE); // alokasi baru
var warna2 = Color(0xFF6200EE); // alokasi baru lagi
print(identical(warna1, warna2)); // false
// Dengan const — satu objek digunakan bersama
const warna1 = Color(0xFF6200EE); // alokasi sekali
const warna2 = Color(0xFF6200EE); // referensi ke objek yang sama
print(identical(warna1, warna2)); // true — zero extra allocation
Dalam aplikasi Flutter dengan ribuan widget yang di-rebuild per detik, perbedaan ini bisa terlihat di profiler.
Aktifkan lint rule
prefer_const_constructorsdanprefer_const_literals_to_create_immutablesdianalysis_options.yaml— editor akan langsung menampilkan peringatan di mana pun kamu seharusnya menggunakanconsttapi belum melakukannya.# analysis_options.yaml linter: rules: - prefer_const_constructors - prefer_const_declarations - prefer_const_literals_to_create_immutables - avoid_redundant_argument_values
Anti-Pattern Konstanta yang Harus Dihindari #
Konstanta final padahal bisa const
#
// ANTI-PATTERN: menggunakan final untuk nilai compile-time
final double pi = 3.14159; // ✗ nilai ini sudah pasti — pakai const
final int maksRetry = 3; // ✗ sama
final String urlBase = 'https://api.example.com'; // ✗ sama
// BENAR: gunakan const untuk nilai compile-time
const double pi = 3.14159;
const int maksRetry = 3;
const String urlBase = 'https://api.example.com';
Konstanta Tanpa Konteks Bermakna #
// ANTI-PATTERN: konstanta dengan nama yang tidak jelas tujuannya
const int n = 100;
const double x = 0.15;
const String s = 'active';
// BENAR: nama yang menjelaskan makna dan konteks penggunaannya
const int maksItemKeranjang = 100;
const double tarifPajakPpn = 0.15;
const String statusAktif = 'active';
Konstanta di dalam Fungsi yang Dipanggil Berulang #
// ANTI-PATTERN: mendefinisikan objek "konstan" di dalam fungsi
// Objek ini dibuat ulang setiap kali fungsi dipanggil
void tampilkanPesan() {
final warna = Color(0xFF6200EE); // ✗ objek baru setiap panggilan
final padding = EdgeInsets.all(16); // ✗ objek baru setiap panggilan
// ...
}
// BENAR: definisikan sebagai const di luar fungsi atau gunakan const langsung
const _warnaPrimer = Color(0xFF6200EE);
const _paddingStandar = EdgeInsets.all(16);
void tampilkanPesan() {
// Atau gunakan const langsung di pemanggilan
final warna = const Color(0xFF6200EE); // ✓ canonical instance
// ...
}
Menggunakan Class dengan Konstruktor Publik sebagai Namespace Konstanta #
// ANTI-PATTERN: kelas dengan constructor yang bisa dipanggil
class Warna {
static const int primer = 0xFF6200EE;
static const int sekunder = 0xFF03DAC6;
// Constructor publik masih bisa dipanggil: Warna() — tidak ada artinya
}
// BENAR: gunakan abstract class atau tambahkan constructor privat
abstract class Warna {
Warna._(); // blok instansiasi secara eksplisit
static const int primer = 0xFF6200EE;
static const int sekunder = 0xFF03DAC6;
}
Ringkasan #
constadalah compile-time — nilainya harus sudah diketahui sebelum program berjalan. Nilai primitif, koleksi literal, dan instance dari kelas denganconstconstructor semuanya bisa menjadiconst.finaladalah runtime — nilainya bisa bergantung pada kondisi saat program berjalan, tapi setelah di-set, tidak bisa diganti.DateTime.now(), hasil fungsi, dan input pengguna semuanya bisa menjadifinal.- Koleksi
constbenar-benar immutable — tidak hanya referensinya, tapi isinya juga tidak bisa dimodifikasi.final Listhanya melindungi referensinya; isinya masih bisa di-add()atau di-remove().- Canonical instances — dua ekspresi
constdengan nilai yang identik dijamin mengacu ke objek yang sama di memori. Ini penting untuk performa di Flutter karena widgetconsttidak di-rebuild saat parent rebuild.constconstructor memerlukan semua propertifinaldan body constructor kosong. Kelas yang bisa mendukungconstconstructor sebaiknya selalu mendefinisikannya.- Organisasi konstanta — kumpulkan dalam
abstract classdengan constructor privat per domain (jaringan, UI, autentikasi) daripada satu file raksasa atau tersebar di setiap file pemakai.- Enum untuk nilai diskrit — gunakan enum (terutama enhanced enum Dart 2.17+) untuk sekumpulan nilai tetap yang terkait, bukan
const Stringatauconst intyang terpisah-pisah.- Aktifkan lint rules
prefer_const_constructorsdanprefer_const_declarations— biarkan tooling yang mengingatkan di manaconstseharusnya digunakan.- Hindari magic number — setiap angka atau string literal yang muncul lebih dari sekali di codebase adalah kandidat konstanta bernama.