Variabel

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});
}
late adalah pengecualian, bukan aturan. Gunakan hanya ketika kamu benar-benar tidak bisa menyediakan nilai di titik deklarasi. Terlalu banyak late dalam 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 #

  • var untuk 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.
  • final untuk variabel yang tidak perlu diubah setelah di-set. Jadikan final sebagai default, ubah ke var hanya jika memang nilainya perlu berubah.
  • const untuk nilai yang sudah pasti sejak compile time — literal, ekspresi literal, dan instance kelas dengan const constructor. Isi koleksi const benar-benar immutable, tidak hanya referensinya.
  • String? vs String — 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 banyak if (x != null) yang verbose.
  • late adalah pengecualian untuk kasus di mana inisialisasi benar-benar tidak bisa dilakukan di titik deklarasi. Terlalu banyak late adalah sinyal desain yang perlu ditinjau.
  • Hindari dynamic — ia mematikan type checker dan memindahkan semua error dari compile time ke runtime. Gunakan Object, 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_CASE untuk konstanta.

← Sebelumnya: Komentar   Berikutnya: Konstanta →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact