Constants #

A constant is not just a variable that happens to never change — it’s an explicit statement to the compiler, the team, and the code’s readers that this value must not change, by design. Dart provides two keywords for this purpose: const and final. Both prevent the value from changing, but their mechanisms and implications are very different. Choosing the wrong one won’t immediately cause a bug, but choosing the right one unlocks compiler optimizations, prevents subtle logical errors, and — especially in Flutter — has a real impact on rendering performance. This article covers both in depth, along with good constant-management patterns for real projects.

The Problem Constants Solve #

Imagine an e-commerce app with discount logic scattered across dozens of files:

// In checkout.dart
double diskon = harga * 0.1;

// In keranjang.dart
double potongan = subtotal * 0.1;

// In laporan.dart
double penguranganHarga = totalBelanja * 0.10;

Three months later, the business policy changes: the discount goes from 10% to 15%. The developer has to find and replace every 0.1 and 0.10 across the whole codebase — with the risk of missing one and causing inconsistencies. Numbers like these are called magic numbers, and this is the problem constants solve.

// ANTI-PATTERN: magic numbers scattered across the codebase
double diskon = harga * 0.1;
double pajakPpn = subtotal * 0.11;
int batasRetry = 3;
int timeoutDetik = 30;

// CORRECT: define once, use everywhere
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;

Besides eliminating magic numbers, constants bring other benefits: the compiler can validate their usage, refactoring becomes a single point of change, and the code becomes self-documenting because the constant name explains the meaning of its value.


const — Compile-Time Constants #

const declares a value that is fully known at compile time — before the program runs. The compiler evaluates the const expression and embeds the result directly into the compiled code, like literally replacing the variable name with its value.

// Primitive values — the most common
const double phi = 3.14159265358979;
const int maksKarakter = 280;           // tweet character limit
const String versiApi = 'v2';
const bool modeProduksi = true;

// Compile-time expressions — all components must be const or literal
const double duaPhi = 2 * phi;          // ✓ phi is already const
const int batasKarakterJudul = maksKarakter ~/ 2; // ✓ maksKarakter is already const
const String urlBase = 'https://api.example.com/' + versiApi; // ✓

// const collections — truly immutable all the way to their contents
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'};

Values that depend on runtime cannot be const — the compiler will reject them immediately:

// All of these are compile errors
const DateTime sekarang = DateTime.now();        // ✗ depends on runtime time
const String inputPengguna = stdin.readLineSync()!; // ✗ depends on input
const int nilaiAcak = Random().nextInt(100);     // ✗ depends on runtime RNG

// Solution: use final for runtime values that must not change
final DateTime waktuMulai = DateTime.now();      // ✓
final String konfigurasi = loadConfig();         // ✓

What Happens Behind the Scenes #

When the compiler encounters const double phi = 3.14159265358979, it doesn’t allocate new memory every time phi is used — it embeds the value 3.14159265358979 directly into the bytecode. This is what makes const fundamentally different from final:

flowchart LR
    subgraph "Compile Time"
        C1["const phi = 3.14159"] --> C2["Compiler evaluates"]
        C2 --> C3["3.14159 embedded\\ninto bytecode"]
    end
    subgraph "Runtime"
        R1["final waktu = DateTime.now()"] --> R2["Program runs"]
        R2 --> R3["Memory allocated\\nValue evaluated"]
    end

const Constructors — Constant Objects #

const isn’t limited to primitive values. Classes can support const constructors, which enable creating fully immutable instances evaluated at compile time.

Requirements for defining a const constructor:

  • All class properties must be final
  • The constructor body must be empty (no runtime logic)
  • All values passed to the constructor must be const-capable
class Titik {
  final double x;
  final double y;

  // const constructor — body must be empty
  const Titik(this.x, this.y);

  // Methods are allowed — they don't change state, only compute
  double jarakKe(Titik lain) {
    final dx = x - lain.x;
    final dy = y - lain.y;
    return (dx * dx + dy * dy);  // squared distance, no sqrt for performance
  }

  @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 constructors can be const too
  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);          // ✓ const instance
  const ujung = Titik(3, 4);          // ✓ const instance
  final titikRuntime = Titik(1.5, 2.5); // ✓ also valid, but not const

  const merah = Warna.merah();        // ✓
  const biru = Warna(0, 0, 255);      // ✓
}
// ANTI-PATTERN: a class that could be const but doesn't define a const constructor
class KonfigurasiWarna {
  final int r;
  final int g;
  final int b;

  KonfigurasiWarna(this.r, this.g, this.b); // ✗ not const — a const opportunity missed
}

// CORRECT: add a const constructor if all properties are final and there's no logic
class KonfigurasiWarna {
  final int r;
  final int g;
  final int b;

  const KonfigurasiWarna(this.r, this.g, this.b); // ✓ supports const
}

Canonical Instances — One Object for Equal Values #

The most unique feature of const constructors is canonical instances: two const expressions with identical values are guaranteed to refer to the exact same object in memory, not two different objects that happen to have equal values.

const a = Titik(0, 0);
const b = Titik(0, 0);
const c = Titik(1, 1);

print(identical(a, b)); // true  — the same single object in memory
print(identical(a, c)); // false — two different objects

// Compare with non-const objects:
final x = Titik(0, 0);
final y = Titik(0, 0);
print(identical(x, y)); // false — two different objects even with equal values

This isn’t just a technical detail — it has huge implications in Flutter. const widgets with the same value don’t need to be recreated on rebuild because Dart guarantees their instances are identical:

// In Flutter — const widgets are not rebuilt when the parent rebuilds
Widget build(BuildContext context) {
  return Column(
    children: [
      const Text('Static label'),           // ✓ not rebuilt
      const Icon(Icons.star, size: 24),    // ✓ not rebuilt
      Text(nilaiDariState),                // this one changes on state updates
    ],
  );
}

final — Runtime Constants #

final declares a variable whose value can only be set once, but the value can be determined at any time — at declaration, in the constructor, or at the first moment it’s needed. It’s a more flexible keyword than const because it can hold values that are only known while the program runs.

// Runtime values that must not change after being set
final DateTime waktuMulaiApp = DateTime.now();
final String idSesi = generateUuid();
final Map<String, dynamic> konfigurasi = loadConfigFromFile('config.json');

// Values that can be computed from runtime data
final int totalHalaman = (jumlahData / itemPerHalaman).ceil();
final String pesanSambutan = 'Welcome, ${pengguna.nama}!';

final on Class Properties #

final is most often encountered as a class property initialized through the constructor — this pattern is the foundation of immutable data class design in Dart:

class Pengguna {
  final String id;
  final String nama;
  final String email;
  final DateTime tanggalDaftar;

  // All final properties are initialized in the constructor
  const Pengguna({
    required this.id,
    required this.nama,
    required this.email,
    required this.tanggalDaftar,
  });

  // Because it's immutable, "changes" are done by creating a new object
  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: final properties can't be changed

  // Create a new object with updated values
  final penggunaDiperbarui = pengguna.salinDengan(nama: 'Budi S.');
  print(penggunaDiperbarui.nama);  // Budi S.
  print(pengguna.nama);            // Budi Santoso — the original object is unchanged
}

final in Initializer Lists #

Dart constructors have an initializer list feature — a block before the constructor body that can initialize final properties with more complex expressions:

class Lingkaran {
  final double jariJari;
  final double luas;
  final double keliling;

  // The initializer list computes area and circumference from 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
}

The Full Difference Between const and final #

Understanding exactly how they differ is the key to choosing the right one in every situation:

Aspectconstfinal
Evaluation timeCompile timeRuntime (when first set)
Runtime value✗ not possible✓ possible
Collection contentsFully immutableMutable (List can .add())
Canonical instance✓ same object in memory✗ new object every time
On class properties✓ (must be static)✓ (instance or static)
Needs static in a class✓ for instance props✗ not needed
Compiler optimizationMaximumPartial
// Illustrating the difference in collection contents
const List<int> listConst = [1, 2, 3];
final List<int> listFinal = [1, 2, 3];

listConst.add(4);   // ✗ runtime error: Unsupported operation: add
listFinal.add(4);   // ✓ works — final reference, mutable contents
print(listFinal);   // [1, 2, 3, 4]
flowchart TD
    A{Is the value already known\\nat compile time?} -- Yes --> B{Can all components\\nbe const?}
    B -- Yes --> C[Use const]
    B -- No --> D[Use final]
    A -- No --> E{Does the value need to\\nchange after being set?}
    E -- Yes --> F[Use var or\\nan explicit type]
    E -- No --> D

Organizing Constants in a Project #

A small project might be fine with defining constants in the files that use them. But as the project grows, constants scattered across many files become hard to manage. There are several common patterns.

Pattern 1: Abstract Class Holding Constants #

The most common approach in Dart — collecting thematic constants in an abstract class so it can’t be instantiated:

// lib/core/constants/app_constants.dart
abstract class AppConstants {
  // Prevent instantiation
  AppConstants._();

  // Network configuration
  static const String urlBase = 'https://api.example.com';
  static const String versiApi = 'v2';
  static const int timeoutDetik = 30;
  static const int maksRetry = 3;

  // UI limits
  static const int maksKarakterJudul = 100;
  static const int maksKarakterDeskripsi = 500;
  static const int itemPerHalaman = 20;

  // Animation durations
  static const Duration durasiAnimasiPendek = Duration(milliseconds: 150);
  static const Duration durasiAnimasiNormal = Duration(milliseconds: 300);
  static const Duration durasiAnimasiLambat = Duration(milliseconds: 500);
}

// Usage
final url = '${AppConstants.urlBase}/${AppConstants.versiApi}/produk';

Pattern 2: Per-Feature Constants #

For large projects with a feature-based architecture, each module/feature can have its own constants file:

// 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;
}

Pattern 3: Top-Level Constants in a Separate File #

A simpler alternative — constants as plain top-level variables, without a wrapping class:

// lib/core/constants.dart

// Network
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;

// Colors (if not using ThemeData)
const int kWarnaUtamaHex = 0xFF6200EE;
const int kWarnaSekunderHex = 0xFF03DAC6;

The k prefix is a convention used by the Flutter SDK itself to distinguish constants from regular variables.

// ANTI-PATTERN: defining the same constant in many places
// In file_a.dart:
const int timeoutDetik = 30;

// In file_b.dart:
const int networkTimeout = 30;  // duplication — one number, two different names

// In file_c.dart:
const int koneksiTimeout = 30;  // duplicated again

// CORRECT: one definition in one place, used everywhere
// In app_constants.dart:
static const int timeoutKoneksi = 30;

// In file_a.dart, file_b.dart, file_c.dart:
import 'app_constants.dart';
// Use AppConstants.timeoutKoneksi

Constants in Enums #

Enums in Dart are a more structured way to define a set of related constant values. Dart 2.17 and later support enhanced enums that can have properties and methods:

// A simple enum
enum StatusPesanan { menunggu, diproses, dikirim, selesai, dibatalkan }

// Enhanced enum — can have properties, constructors, and methods
enum MetodePembayaran {
  transfer(kode: 'TF', labelTampilan: 'Bank Transfer', biayaAdmin: 2500),
  kartuKredit(kode: 'CC', labelTampilan: 'Credit Card', biayaAdmin: 0),
  qris(kode: 'QR', labelTampilan: 'QRIS', biayaAdmin: 0),
  tunai(kode: 'CS', labelTampilan: 'Pay on Delivery', 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);  // Bank Transfer
  print(metode.biayaAdmin);     // 2500
  print(metode.gratis);         // false

  // Iterate over all enum values
  for (final m in MetodePembayaran.values) {
    print('${m.labelTampilan}: ${m.gratis ? "free" : "Rp${m.biayaAdmin}"}');
  }
}
// ANTI-PATTERN: using String or int instead of an enum
const String statusMenunggu = 'WAITING';
const String statusDiproses = 'PROCESSING';
// No compiler guarantee that only these values are valid
void prosesOrder(String status) {
  if (status == 'WAITNG') { ... }  // typo — the compiler can't detect it
}

// CORRECT: use an enum for a fixed set of related values
enum StatusPesanan { menunggu, diproses, dikirim, selesai, dibatalkan }

void prosesOrder(StatusPesanan status) {
  if (status == StatusPesanan.menunggu) { ... }  // ✓ the compiler validates
}

Performance Implications — Especially in Flutter #

The choice between const, final, and regular variables has a real impact on Flutter app performance. This isn’t premature optimization — it’s a habit to build from the start.

const Widgets Are Not Rebuilt #

Flutter rebuilds the widget tree every time state changes. Widgets that don’t change shouldn’t be rebuilt along with it — and the way to tell Flutter a widget won’t change is by using const:

// ANTI-PATTERN: static widgets without const — rebuilt every time the parent rebuilds
class HalamanUtama extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Online Store'),          // ✗ rebuilt every time
        actions: [
          Icon(Icons.shopping_cart),          // ✗ rebuilt every time
          SizedBox(width: 16),               // ✗ rebuilt every time
        ],
      ),
      body: Column(
        children: [
          Text('Welcome'),                    // ✗ rebuilt every time
          _kontenDinamis(),                   // this one genuinely needs rebuilding
        ],
      ),
    );
  }
}

// CORRECT: static widgets use const — not rebuilt
class HalamanUtama extends StatefulWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Online Store'),   // ✓ not rebuilt
        actions: const [
          Icon(Icons.shopping_cart),          // ✓ not rebuilt
          SizedBox(width: 16),               // ✓ not rebuilt
        ],
      ),
      body: Column(
        children: [
          const Text('Welcome'),             // ✓ not rebuilt
          _kontenDinamis(),                   // this one genuinely needs rebuilding
        ],
      ),
    );
  }
}

Canonical Instances Save Memory Allocations #

Because const produces canonical instances, calling the same constructor with the same values over and over doesn’t allocate new objects:

// Without const — every call creates a new object on the heap
var warna1 = Color(0xFF6200EE);  // new allocation
var warna2 = Color(0xFF6200EE);  // another new allocation
print(identical(warna1, warna2)); // false

// With const — one shared object
const warna1 = Color(0xFF6200EE);  // allocated once
const warna2 = Color(0xFF6200EE);  // reference to the same object
print(identical(warna1, warna2));  // true — zero extra allocation

In a Flutter app with thousands of widgets rebuilt per second, this difference shows up in the profiler.

Enable the prefer_const_constructors and prefer_const_literals_to_create_immutables lint rules in analysis_options.yaml — the editor will immediately show warnings wherever you should be using const but aren’t.

# analysis_options.yaml
linter:
  rules:
    - prefer_const_constructors
    - prefer_const_declarations
    - prefer_const_literals_to_create_immutables
    - avoid_redundant_argument_values

Constant Anti-Patterns to Avoid #

final When It Could Be const #

// ANTI-PATTERN: using final for compile-time values
final double pi = 3.14159;          // ✗ this value is certain — use const
final int maksRetry = 3;            // ✗ same
final String urlBase = 'https://api.example.com'; // ✗ same

// CORRECT: use const for compile-time values
const double pi = 3.14159;
const int maksRetry = 3;
const String urlBase = 'https://api.example.com';

Constants Without Meaningful Context #

// ANTI-PATTERN: constants with names that don't reveal their purpose
const int n = 100;
const double x = 0.15;
const String s = 'active';

// CORRECT: names that explain the meaning and usage context
const int maksItemKeranjang = 100;
const double tarifPajakPpn = 0.15;
const String statusAktif = 'active';

Constants Inside Functions Called Repeatedly #

// ANTI-PATTERN: defining "constant" objects inside a function
// This object is recreated every time the function is called
void tampilkanPesan() {
  final warna = Color(0xFF6200EE);   // ✗ new object on every call
  final padding = EdgeInsets.all(16); // ✗ new object on every call
  // ...
}

// CORRECT: define it as const outside the function or use const directly
const _warnaPrimer = Color(0xFF6200EE);
const _paddingStandar = EdgeInsets.all(16);

void tampilkanPesan() {
  // Or use const directly in the call
  final warna = const Color(0xFF6200EE);   // ✓ canonical instance
  // ...
}

Using a Class with a Public Constructor as a Constant Namespace #

// ANTI-PATTERN: a class with a callable constructor
class Warna {
  static const int primer = 0xFF6200EE;
  static const int sekunder = 0xFF03DAC6;
  // The public constructor can still be called: Warna() — meaningless
}

// CORRECT: use an abstract class or add a private constructor
abstract class Warna {
  Warna._(); // explicitly blocks instantiation
  static const int primer = 0xFF6200EE;
  static const int sekunder = 0xFF03DAC6;
}

Summary #

  • const is compile-time — its value must be known before the program runs. Primitive values, collection literals, and instances of classes with const constructors can all be const.
  • final is runtime — its value can depend on conditions while the program runs, but once set, it can’t be replaced. DateTime.now(), function results, and user input can all be final.
  • const collections are truly immutable — not just the reference, but the contents can’t be modified either. A final List only protects the reference; its contents can still be .add()ed or .remove()d.
  • Canonical instances — two const expressions with identical values are guaranteed to refer to the same object in memory. This matters for Flutter performance because const widgets aren’t rebuilt when the parent rebuilds.
  • const constructors require all properties to be final and the constructor body to be empty. A class that can support a const constructor should always define one.
  • Organizing constants — group them in an abstract class with a private constructor per domain (network, UI, authentication) rather than one giant file or scattered across every consumer file.
  • Enums for discrete values — use enums (especially Dart 2.17+ enhanced enums) for a fixed set of related values, not scattered const String or const int declarations.
  • Enable the lint rules prefer_const_constructors and prefer_const_declarations — let the tooling remind you where const should be used.
  • Avoid magic numbers — every number or string literal appearing more than once in the codebase is a candidate for a named constant.

← Previous: Variables   Next: Data Types →

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