Variabel #
Variabel adalah tempat menyimpan data di memori selama program berjalan. Di Dart, cara kamu mendeklarasikan variabel bukan sekadar pilihan gaya — ia menentukan apakah nilai bisa diubah, apakah nilainya bisa null, dan apakah compiler bisa memverifikasi kebenarannya sebelum program dijalankan. Dart menyediakan empat kata kunci utama untuk deklarasi variabel (var, tipe eksplisit, final, const), ditambah modifier late untuk kasus khusus inisialisasi tertunda. Memahami kapan menggunakan masing-masing adalah kunci untuk menulis kode Dart yang aman, ekspresif, dan mudah dipelihara.
Cara Dart Menyimpan Variabel #
Sebelum masuk ke sintaks, ada satu konsep fundamental yang perlu dipahami: di Dart, semua variabel menyimpan referensi ke objek, bukan nilai itu sendiri. Bahkan tipe primitif seperti int dan bool adalah objek di Dart — tidak ada perbedaan antara primitive type dan object type seperti di Java.
int angka = 42;
// Variabel 'angka' menyimpan referensi ke objek int dengan nilai 42
// bukan nilai 42 secara langsung di stack seperti di C/Java
Implikasinya: semua variabel bisa memiliki nilai null secara teori — null adalah referensi yang tidak menunjuk ke objek manapun. Inilah mengapa Dart memerlukan sistem null safety yang eksplisit untuk menjamin keamanan program di compile time, bukan hanya saat runtime.
flowchart LR
subgraph Stack
V1["var nama"]
V2["int umur"]
V3["String? kota"]
end
subgraph Heap
O1["'Budi'"]
O2["42"]
O3["null"]
end
V1 --> O1
V2 --> O2
V3 --> O3
Deklarasi dengan var
#
Kata kunci var adalah cara paling ringkas mendeklarasikan variabel ketika tipenya sudah jelas dari nilai yang diberikan. Compiler Dart menyimpulkan (infer) tipe secara otomatis — setelah tipe disimpulkan, variabel tersebut hanya bisa menyimpan nilai dari tipe yang sama.
var nama = 'Budi'; // Dart menyimpulkan: String
var umur = 25; // Dart menyimpulkan: int
var tinggi = 1.75; // Dart menyimpulkan: double
var aktif = true; // Dart menyimpulkan: bool
var skor = [90, 85, 92]; // Dart menyimpulkan: List<int>
// Setelah tipe disimpulkan, tidak bisa diganti tipe lain
nama = 'Siti'; // ✓ String ke String
nama = 42; // ✗ error: int tidak bisa diassign ke String
var bukanlah dynamic typing — Dart tetap statically typed. Bedanya dengan tipe eksplisit hanya pada keterbacaan source code, bukan pada perilaku runtime.
// ANTI-PATTERN: menggunakan var ketika tipe tidak obvious dari nilai
var hasil = prosesData(); // apa tipe kembalian prosesData()?
var x = a * b + c / d; // tipe apa yang dihasilkan ekspresi ini?
var config = ambilKonfigurasi(); // Map? Object? String? tidak jelas
// BENAR: gunakan tipe eksplisit ketika tipe tidak obvious
double hasilHitung = a * b + c / d;
Map<String, dynamic> config = ambilKonfigurasi();
List<Produk> daftarProduk = repository.ambilSemua();
Panduan praktis: gunakan var ketika nilai awal ditulis langsung di baris yang sama dan tipenya sudah jelas bagi pembaca dalam sekali lihat. Gunakan tipe eksplisit untuk segala hal lainnya — terutama deklarasi tanpa nilai awal, parameter fungsi, dan return type.
Deklarasi dengan Tipe Eksplisit #
Menuliskan tipe secara eksplisit memberikan dokumentasi inline yang tidak bisa usang seperti komentar — karena compiler akan langsung komplain jika ada ketidakcocokan.
String nama = 'Budi';
int umur = 25;
double gaji = 8_500_000.0; // underscore sebagai pemisah ribuan — valid di Dart
bool sudahVerifikasi = false;
// Tipe koleksi dengan generic
List<String> kota = ['Jakarta', 'Bandung', 'Surabaya'];
Map<String, int> skor = {'matematika': 90, 'fisika': 85};
Set<String> tag = {'dart', 'flutter', 'mobile'};
Tipe eksplisit wajib digunakan dalam beberapa konteks:
// 1. Deklarasi tanpa nilai awal (tidak bisa pakai var)
String namaLengkap; // ✓ tipe eksplisit
// var namaLengkap; // ✗ var tanpa nilai awal: tipe jadi dynamic
// 2. Parameter fungsi
void sapa(String nama, int umur) { ... } // ✓
// void sapa(var nama, var umur) { ... } // ✗ tidak valid
// 3. Return type fungsi
String formatNama(String depan, String belakang) { // ✓
return '$depan $belakang';
}
// 4. Properti kelas
class Pengguna {
String nama; // ✓ tipe eksplisit di properti kelas
int umur;
Pengguna(this.nama, this.umur);
}
final — Tidak Bisa Diubah Setelah Di-set
#
final mendeklarasikan variabel yang hanya bisa di-set nilainya sekali. Setelah diinisialisasi, nilai tidak bisa diganti — tapi jika nilainya adalah objek mutable (seperti List), isi objek tersebut masih bisa dimodifikasi.
final String nama = 'Budi';
// nama = 'Siti'; // ✗ error: final variable tidak bisa di-assign ulang
final umur = 25; // tipe diinferensikan: int
// Inisialisasi bisa ditunda ke runtime (berbeda dari const)
final DateTime sekarang = DateTime.now(); // ✓ nilai runtime
final List<String> kota = ['Jakarta', 'Bandung'];
// Variabel final sendiri tidak bisa diganti, tapi isi list masih bisa
kota.add('Surabaya'); // ✓ isi list bisa dimodifikasi
kota = ['Medan']; // ✗ error: referensi final tidak bisa diganti
Kapan final Lebih Tepat dari var
#
Sebagai prinsip umum: deklarasikan semua variabel sebagai final kecuali kamu memang perlu mengubah nilainya. Ini bukan sekadar gaya — ini membuat intent kode lebih jelas dan mencegah perubahan yang tidak disengaja.
// ANTI-PATTERN: semua variabel var padahal sebagian tidak pernah diubah
var nama = 'Budi'; // nama tidak pernah diubah di sisa kode
var umur = 25; // umur juga tidak pernah diubah
var counter = 0; // counter diubah di dalam loop — ini wajar pakai var
// BENAR: final untuk yang tidak berubah, var/tipe eksplisit untuk yang berubah
final String nama = 'Budi';
final int umur = 25;
int counter = 0; // ini memang perlu diubah
final juga sangat umum digunakan di properti kelas untuk menjamin immutability setelah konstruktor selesai:
class Produk {
final String id; // tidak boleh berubah setelah dibuat
final String nama;
double harga; // harga bisa berubah (misal saat promosi)
int stok; // stok berubah saat transaksi
Produk({
required this.id,
required this.nama,
required this.harga,
this.stok = 0,
});
}
const — Konstanta Compile-Time
#
const adalah versi yang lebih ketat dari final. Nilainya harus sudah diketahui pada saat kompilasi — bukan runtime. Artinya, nilai const tidak bisa bergantung pada input pengguna, hasil fungsi, atau waktu eksekusi.
const double pi = 3.14159265358979;
const int maksimalRetry = 3;
const String versiApp = '1.0.0';
// Ekspresi const juga valid — asalkan semua komponennya const
const double duaPi = 2 * pi; // ✓ semua komponen sudah diketahui
const int batasMaksimal = 10 * 10; // ✓ literal aritmatika
Nilai yang bergantung pada runtime tidak bisa dijadikan const:
// ANTI-PATTERN: mencoba membuat nilai runtime menjadi const
const DateTime sekarang = DateTime.now(); // ✗ error: DateTime.now() adalah runtime value
const String input = stdin.readLineSync()!; // ✗ error: input pengguna adalah runtime
// BENAR: gunakan final untuk nilai yang hanya bisa ditentukan saat runtime
final DateTime sekarang = DateTime.now(); // ✓
final String input = stdin.readLineSync()!; // ✓
const pada Objek dan Koleksi
#
const bisa diterapkan tidak hanya pada variabel, tapi juga langsung pada nilai — termasuk list, map, dan instance kelas yang mendukungnya:
// Objek const — satu instance dibagi di seluruh program (canonical instance)
const listKonstanta = [1, 2, 3]; // List<int> yang immutable sepenuhnya
const mapKonstanta = {'a': 1}; // Map yang immutable sepenuhnya
// Berbeda dengan final list yang hanya referensinya yang tidak bisa diganti
final listFinal = [1, 2, 3];
listFinal.add(4); // ✓ isi list final bisa dimodifikasi
listKonstanta.add(4); // ✗ error: const list tidak bisa dimodifikasi sama sekali
// Kelas yang mendukung const constructor
class Warna {
final int r, g, b;
const Warna(this.r, this.g, this.b);
}
// Instance const — dibuat sekali saat compile, tidak dibuat ulang saat runtime
const Warna merah = Warna(255, 0, 0);
const Warna hijau = Warna(0, 255, 0);
// Identitas referensi terjamin sama untuk nilai const yang identik
const a = Warna(255, 0, 0);
const b = Warna(255, 0, 0);
print(identical(a, b)); // true — satu objek yang sama
Pemanfaatan const di Flutter sangat penting untuk performa — widget const tidak di-rebuild saat parent rebuild karena Dart menjamin instance-nya identik.
Perbandingan var, final, dan const
#
| Aspek | var / tipe eksplisit |
final |
const |
|---|---|---|---|
| Bisa diubah | ✓ | ✗ setelah di-set | ✗ |
| Waktu inisialisasi | Runtime | Runtime | Compile time |
| Tipe diinferensikan | ✓ (dengan var) |
✓ | ✓ |
| Nilai runtime | ✓ | ✓ | ✗ |
| Isi objek mutable | ✓ | ✓ | ✗ |
| Canonical instance | ✗ | ✗ | ✓ |
flowchart TD
A{Nilai perlu berubah\nsetelah di-set?} -- Ya --> B[Gunakan var atau\ntipe eksplisit]
A -- Tidak --> C{Nilai sudah diketahui\nsaat compile time?}
C -- Ya --> D[Gunakan const]
C -- Tidak --> E[Gunakan final]
Null Safety dan Variabel Nullable #
Sejak Dart 2.12, semua variabel bersifat non-nullable secara default — compiler menjamin variabel non-nullable tidak pernah bernilai null saat runtime. Untuk mengizinkan null, kamu harus secara eksplisit menambahkan ? setelah tipe.
// Non-nullable — dijamin tidak pernah null oleh compiler
String nama = 'Budi';
int umur = 25;
// nama = null; // ✗ error kompilasi
// Nullable — bisa menyimpan null, harus ditangani sebelum digunakan
String? alamat; // nilai awal: null
int? nilaiUjian; // nilai awal: null
alamat = 'Jl. Merdeka 1';
nilaiUjian = null; // ✓ valid untuk nullable
Mengakses Nilai Nullable dengan Aman #
Sebelum menggunakan nilai nullable, Dart mengharuskan kamu menangani kemungkinan null secara eksplisit. Ada beberapa cara:
String? kota;
// 1. Operator ?? — berikan nilai default jika null
String tampilan = kota ?? 'Kota tidak diketahui';
// 2. Operator ?. — akses properti/method hanya jika tidak null
int? panjangKota = kota?.length; // hasilnya int? bukan int
// 3. if null check — Dart melakukan type promotion setelah pengecekan
if (kota != null) {
// Di dalam blok ini, Dart tahu 'kota' pasti String (bukan String?)
print(kota.length); // ✓ aman, tidak perlu ?.
print(kota.toUpperCase());
}
// 4. Null assertion operator ! — paksa non-null (gunakan dengan hati-hati)
print(kota!.length); // throws jika kota ternyata null saat runtime
// ANTI-PATTERN: menggunakan ! secara sembarangan tanpa menjamin nilainya
String? input = ambilInputPengguna();
print(input!.length); // ✗ akan crash jika pengguna tidak mengisi apapun
// BENAR: tangani null secara eksplisit
String? input = ambilInputPengguna();
if (input == null || input.isEmpty) {
print('Input tidak boleh kosong');
return;
}
// Di sini, Dart tahu input pasti non-null
print(input.length); // ✓ aman
Operator ??= — Assign Jika Null
#
String? nama;
nama ??= 'Tamu'; // assign 'Tamu' hanya jika nama masih null
print(nama); // Tamu
nama ??= 'Admin'; // tidak melakukan apa-apa karena nama sudah 'Tamu'
print(nama); // Tamu
late — Inisialisasi Tertunda
#
Kata kunci late memungkinkan mendeklarasikan variabel non-nullable tanpa memberikan nilai awal seketika — dengan janji bahwa nilai akan diisi sebelum variabel pertama kali diakses. Jika janji ini dilanggar (variabel diakses sebelum diisi), Dart akan melempar LateInitializationError saat runtime.
late String koneksiDatabase;
void inisialisasiApp() {
koneksiDatabase = 'postgresql://localhost/mydb';
}
void main() {
inisialisasiApp();
print(koneksiDatabase); // ✓ aman, sudah diinisialisasi
}
Kapan late Diperlukan
#
Ada tiga skenario umum di mana late benar-benar dibutuhkan:
1. Variabel yang tidak bisa diinisialisasi di deklarasi tapi bukan nullable
class FormPendaftaran extends StatefulWidget {
// Controller ini harus diinisialisasi di initState(), bukan di konstruktor
late TextEditingController namaController;
late TextEditingController emailController;
@override
void initState() {
super.initState();
namaController = TextEditingController();
emailController = TextEditingController();
}
@override
void dispose() {
namaController.dispose();
emailController.dispose();
super.dispose();
}
}
2. Lazy initialization — inisialisasi mahal yang hanya dilakukan jika benar-benar dibutuhkan
class LaporanBulanan {
// Data ini mahal untuk dihitung — hanya dihitung sekali saat pertama diakses
late final List<Transaksi> _transaksiTerfilter = _hitungTransaksi();
List<Transaksi> _hitungTransaksi() {
// Operasi berat: query database, filter, sort
print('Menghitung transaksi...'); // hanya muncul sekali
return repository.ambilSemua().where((t) => t.bulan == bulanIni).toList();
}
}
3. late final — inisialisasi sekali tapi setelah konstruktor
class KoneksiDatabase {
late final String connectionString;
void hubungkan(String host, String dbName) {
connectionString = 'postgresql://$host/$dbName'; // hanya bisa di-set sekali
}
}
// ANTI-PATTERN: menggunakan late untuk menghindari null safety tanpa alasan jelas
class Pengguna {
late String nama; // ✗ lebih baik nullable String? atau required di konstruktor
late int umur; // ✗ mengundang LateInitializationError yang sulit di-debug
}
// BENAR: gunakan null safety secara eksplisit
class Pengguna {
String? nama; // nullable jika memang bisa tidak ada
int umur;
Pengguna({required this.umur, this.nama});
}
lateadalah pengecualian, bukan aturan. Gunakan hanya ketika kamu benar-benar tidak bisa menyediakan nilai di titik deklarasi. Terlalu banyaklatedalam satu kelas adalah sinyal bahwa desain kelasnya perlu ditinjau ulang.
Scope Variabel #
Scope menentukan di mana sebuah variabel bisa diakses. Dart menggunakan lexical scoping — variabel bisa diakses dari dalam blok tempat ia dideklarasikan dan semua blok yang bersarang di dalamnya, tapi tidak dari luar.
// Top-level variable — bisa diakses dari seluruh file
int hitungGlobal = 0;
class Kalkulator {
// Instance variable — bisa diakses dari semua method di kelas ini
double _memori = 0;
double tambah(double a, double b) {
// Local variable — hanya ada di dalam fungsi ini
double hasil = a + b;
if (hasil > 1000) {
// Block variable — hanya ada di dalam blok if ini
String pesan = 'Hasil sangat besar: $hasil';
print(pesan);
}
// print(pesan); // ✗ error: 'pesan' tidak ada di scope sini
return hasil;
}
}
Variable Shadowing #
Dart memperbolehkan variabel di scope dalam memiliki nama yang sama dengan variabel di scope luar — kondisi ini disebut shadowing. Ini legal tapi sering menjadi sumber kebingungan:
String nama = 'Global';
void contohShadowing() {
String nama = 'Lokal'; // menyembunyikan variabel global 'nama'
print(nama); // Lokal
{
String nama = 'Block'; // menyembunyikan 'Lokal'
print(nama); // Block
}
print(nama); // Lokal — kembali ke scope fungsi
}
// ANTI-PATTERN: shadowing yang tidak disengaja menyebabkan bug halus
class PenghitungSkor {
int skor = 0;
void tambahPoin(int skor) { // parameter 'skor' menyembunyikan field 'skor'
skor += 10; // ini mengubah parameter, BUKAN field!
// Field this.skor tidak berubah — bug yang sulit dilacak
}
}
// BENAR: gunakan nama yang berbeda atau this. untuk membedakan
class PenghitungSkor {
int skor = 0;
void tambahPoin(int poin) { // nama parameter berbeda
skor += poin; // jelas: 'skor' adalah field
}
// Atau jika memang harus sama nama:
void setSkor(int skor) {
this.skor = skor; // this. menunjuk ke field secara eksplisit
}
}
Closure dan Capture #
Fungsi di Dart bisa “menangkap” variabel dari scope di sekitarnya — ini disebut closure:
Function buatPenghitung(int mulaiDari) {
int hitung = mulaiDari; // variabel ini di-capture oleh closure
return () {
hitung++; // mengakses dan memodifikasi variabel luar
return hitung;
};
}
void main() {
var hitungA = buatPenghitung(0);
var hitungB = buatPenghitung(10);
print(hitungA()); // 1
print(hitungA()); // 2
print(hitungB()); // 11 — state masing-masing closure terpisah
print(hitungA()); // 3
}
Type Inference secara Mendalam #
Dart melakukan type inference tidak hanya untuk deklarasi variabel sederhana, tapi juga untuk ekspresi yang lebih kompleks:
// Inference dari literal
var angka = 42; // int
var teks = 'halo'; // String
var daftar = [1, 2, 3]; // List<int>
var peta = {'a': 1}; // Map<String, int>
// Inference dari ekspresi
var hasil = 10 / 3; // double (bukan int, meski keduanya int)
var gabung = [1, 2] + [3, 4]; // ✗ tidak valid — Dart tidak overload operator +
// Inference di generic
var pasangan = {'nama': 'Budi', 'umur': 25};
// Dart menyimpulkan: Map<String, Object> karena nilai campuran String dan int
// ANTI-PATTERN: var pada deklarasi tanpa nilai — hasilnya dynamic
var tanpaNilai; // ✗ Dart menyimpulkan dynamic, kehilangan type safety
tanpaNilai = 'teks'; // ✓ valid
tanpaNilai = 42; // ✓ juga valid — tapi ini membingungkan
tanpaNilai = [1, 2, 3]; // ✓ juga valid — sama sekali tidak type-safe
// BENAR: gunakan tipe eksplisit jika tidak memberikan nilai awal
String? namaDepan; // jelas tipenya String?
int? skorUjian; // jelas tipenya int?
dynamic vs Object vs var
#
Ketiganya sering membingungkan karena terlihat mirip, tapi memiliki perbedaan penting:
// dynamic — matikan type checker sepenuhnya (hindari jika bisa)
dynamic apa = 'teks';
apa = 42; // ✓ valid
apa.metodeTidakAda(); // ✓ valid saat compile, ✗ crash saat runtime
// Object — supertype semua tipe non-nullable (type-safe, tapi butuh cast)
Object sesuatu = 'teks';
sesuatu = 42; // ✓ valid
// sesuatu.length; // ✗ error kompilasi: Object tidak punya .length
(sesuatu as String).length; // ✓ cast eksplisit diperlukan
// var — tipe diinferensikan, tetap type-safe setelah inferensi
var inferensi = 'teks'; // disimpulkan: String
// inferensi = 42; // ✗ error: int tidak bisa ke String
inferensi.length; // ✓ Dart tahu ini String, .length valid
| Kata kunci | Type checking | Nilai bisa berganti tipe | Aman |
|---|---|---|---|
var |
✓ penuh setelah inferensi | ✗ | ✓ |
Object |
✓ penuh (butuh cast) | ✓ | ✓ |
dynamic |
✗ dinonaktifkan | ✓ | ✗ |
// ANTI-PATTERN: menggunakan dynamic sebagai cara mudah menghindari tipe
dynamic data = ambilDariApi();
print(data.nama); // crash saat runtime jika API berubah format
// BENAR: definisikan tipe yang jelas atau gunakan pattern matching
Map<String, dynamic> data = ambilDariApi();
String nama = data['nama'] as String; // cast eksplisit dengan pesan error yang jelas
Konvensi Penamaan Variabel #
Dart memiliki konvensi penamaan yang sudah distandarisasi dan diikuti oleh seluruh ekosistem:
// ✓ lowerCamelCase untuk variabel dan parameter
String namaLengkap = 'Budi Santoso';
int jumlahKunjungan = 0;
bool sudahLogin = false;
// ✓ _lowerCamelCase untuk variabel dan method private
String _tokenSesi = '';
int _hitungInternal = 0;
// ✓ UpperCamelCase untuk nama kelas dan tipe
class PenggunaPremium { ... }
typedef KallbackData = void Function(String data);
// ✓ lowerCamelCase untuk konstanta (berbeda dari Java/C yang pakai SCREAMING_CASE)
const int batasUlangCoba = 3; // ✓ Dart style
// const int BATAS_ULANG_COBA = 3; // ✗ bukan Dart style
// ✓ Nama yang deskriptif, hindari singkatan yang tidak umum
int jumlahPengguna = 100; // ✓
int jlhPngg = 100; // ✗ singkatan tidak perlu
int n = 100; // ✗ terlalu singkat, tidak bermakna
// ANTI-PATTERN: nama variabel yang tidak bermakna
var a = hitungTotal();
var b = ambilPengguna();
var c = a * 1.1;
// BENAR: nama yang menjelaskan maksud
double subtotal = hitungTotal();
Pengguna penggunaSaatIni = ambilPengguna();
double totalDenganPajak = subtotal * 1.1;
Ringkasan #
varuntuk variabel yang tipenya obvious dari nilai awal di baris yang sama. Setelah tipe disimpulkan, variabel bersifat statically typed — bukan dynamic.- Tipe eksplisit untuk deklarasi tanpa nilai awal, parameter fungsi, return type, dan situasi apapun di mana tipenya tidak langsung jelas bagi pembaca.
finaluntuk variabel yang tidak perlu diubah setelah di-set. Jadikanfinalsebagai default, ubah kevarhanya jika memang nilainya perlu berubah.constuntuk nilai yang sudah pasti sejak compile time — literal, ekspresi literal, dan instance kelas dengan const constructor. Isi koleksiconstbenar-benar immutable, tidak hanya referensinya.String?vsString— tambahkan?hanya untuk variabel yang memang bisa tidak memiliki nilai. Non-nullable adalah default yang aman; nullable adalah pengecualian yang harus ditangani secara eksplisit.- Operator null-aware (
??,?.,??=) adalah cara idiomatis Dart untuk menangani nullable tanpa banyakif (x != null)yang verbose.lateadalah pengecualian untuk kasus di mana inisialisasi benar-benar tidak bisa dilakukan di titik deklarasi. Terlalu banyaklateadalah sinyal desain yang perlu ditinjau.- Hindari
dynamic— ia mematikan type checker dan memindahkan semua error dari compile time ke runtime. GunakanObject, generics, atau union type yang tepat sebagai gantinya.- Lexical scoping — variabel hanya bisa diakses dari blok tempat ia dideklarasikan ke dalam, tidak dari luar. Hindari shadowing yang tidak disengaja dengan memilih nama variabel yang berbeda.
- lowerCamelCase untuk semua nama variabel, termasuk konstanta — ini berbeda dari bahasa lain yang menggunakan
SCREAMING_CASEuntuk konstanta.