Skip to main content

Static Members

Static members belong to the class itself, not to any particular instance. They're shared across all instances and accessible without creating an object. Used well, they are a powerful tool for constants, utilities, factories, and controlled shared state.


Static Fields

class Counter {
// Static field — ONE copy shared by ALL instances
static int _total = 0;
static const int maxCount = 1000; // static constant

final int id; // instance field — EACH object has its own

Counter() : id = ++_total {
if (_total > maxCount) {
throw StateError('Cannot create more than $maxCount counters');
}
}

static int get total => _total;
static void resetAll() => _total = 0;


String toString() => 'Counter#$id (total: $_total)';
}

var a = Counter();
var b = Counter();
var c = Counter();

print(Counter.total); // 3 — accessed on the class, not an instance
print(a.id); // 1
print(b.id); // 2
Counter.resetAll();
print(Counter.total); // 0

Static Constants

Static constants are the right place for class-scoped magic values:

class HttpStatus {
// Prevent instantiation — this class is a namespace for constants
HttpStatus._();

static const int ok = 200;
static const int created = 201;
static const int noContent = 204;
static const int badRequest = 400;
static const int unauthorized = 401;
static const int forbidden = 403;
static const int notFound = 404;
static const int unprocessableEntity = 422;
static const int tooManyRequests = 429;
static const int internalServerError = 500;
static const int serviceUnavailable = 503;

static const Set<int> successCodes = {200, 201, 202, 204};
static const Set<int> clientErrors = {400, 401, 403, 404, 422, 429};
static const Set<int> serverErrors = {500, 502, 503, 504};

static bool isSuccess(int code) => successCodes.contains(code);
static bool isClientError(int code) => code >= 400 && code < 500;
static bool isServerError(int code) => code >= 500;

static String message(int code) => switch (code) {
200 => 'OK',
201 => 'Created',
204 => 'No Content',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
422 => 'Unprocessable Entity',
429 => 'Too Many Requests',
500 => 'Internal Server Error',
503 => 'Service Unavailable',
int c when c >= 200 && c < 300 => '2xx Success',
int c when c >= 400 && c < 500 => '4xx Client Error',
int c when c >= 500 => '5xx Server Error',
_ => 'Unknown',
};
}

print(HttpStatus.message(404)); // Not Found
print(HttpStatus.isSuccess(201)); // true
print(HttpStatus.isServerError(503)); // true

Static Methods — Utility Classes

import 'dart:math' as math;

class MathUtils {
MathUtils._(); // prevent instantiation

static const double pi = 3.14159265358979;
static const double e = 2.71828182845905;
static const double phi = 1.61803398874989; // golden ratio

// Numeric utilities
static double circleArea(double r) => pi * r * r;
static double sphereVolume(double r) => (4 / 3) * pi * r * r * r;
static double hypotenuse(double a, double b) =>
math.sqrt(a * a + b * b);

static int gcd(int a, int b) => b == 0 ? a : gcd(b, a % b);
static int lcm(int a, int b) => (a * b).abs() ~/ gcd(a, b);

static bool isPrime(int n) {
if (n < 2) return false;
if (n == 2) return true;
if (n.isEven) return false;
for (var i = 3; i * i <= n; i += 2) {
if (n % i == 0) return false;
}
return true;
}

static List<int> primeFactors(int n) {
final factors = <int>[];
for (var i = 2; i * i <= n; i++) {
while (n % i == 0) { factors.add(i); n ~/= i; }
}
if (n > 1) factors.add(n);
return factors;
}

static T clamp<T extends Comparable<T>>(T value, T min, T max) =>
value.compareTo(min) < 0 ? min
: value.compareTo(max) > 0 ? max
: value;
}

print(MathUtils.isPrime(97)); // true
print(MathUtils.primeFactors(360)); // [2, 2, 2, 3, 3, 5]
print(MathUtils.lcm(12, 18)); // 36
print(MathUtils.clamp(15, 0, 10)); // 10
print(MathUtils.hypotenuse(3, 4)); // 5.0

Static Factory Methods

The factory constructor is the standard Dart pattern, but static factory methods offer more flexibility — they can have any name, return nullable, and be overridden in subclasses:

class Temperature {
final double _celsius;
const Temperature._(this._celsius);

// Static factory methods — named, clear, flexible
static Temperature celsius(double c) => Temperature._(c);
static Temperature fahrenheit(double f) => Temperature._((f - 32) * 5 / 9);
static Temperature kelvin(double k) => Temperature._(k - 273.15);

// Safe parse — returns null instead of throwing
static Temperature? tryParse(String s) {
final n = double.tryParse(s);
return n == null ? null : Temperature._(n);
}

double get celsius => _celsius;
double get fahrenheit => _celsius * 9 / 5 + 32;
double get kelvin => _celsius + 273.15;


String toString() => '${_celsius.toStringAsFixed(1)}°C';
}

var boiling = Temperature.celsius(100);
var bodyTemp = Temperature.fahrenheit(98.6);
var absolute = Temperature.kelvin(0);

print(boiling); // 100.0°C
print(bodyTemp.celsius.toStringAsFixed(1)); // 37.0
print(absolute.celsius.toStringAsFixed(2)); // -273.15

Singleton Pattern

// Classic Dart singleton using static + private constructor
class AppConfig {
static final AppConfig _instance = AppConfig._internal();

factory AppConfig() => _instance; // redirects to cached instance
AppConfig._internal() {
// Initialization logic runs exactly once
_loadDefaults();
}

// Configuration state
String apiBase = 'https://api.example.com';
String apiVersion = 'v2';
bool debugMode = false;
Duration timeout = const Duration(seconds: 30);

void _loadDefaults() { /* read from environment */ }

Uri endpoint(String path) =>
Uri.parse('$apiBase/$apiVersion$path');
}

// Always the same instance
var config1 = AppConfig();
var config2 = AppConfig();
print(identical(config1, config2)); // true

config1.debugMode = true;
print(config2.debugMode); // true — same object!

Static State: Pitfalls & Best Practices

// ⚠️ Pitfall 1: Static state persists across tests
class UserSession {
static User? _current;
static User? get current => _current;
static void login(User u) => _current = u;
static void logout() => _current = null;
}

// In tests — previous test's login leaks into next test!
// Fix: call UserSession.logout() in tearDown()

// ⚠️ Pitfall 2: Static mutable state is global state
// Treat it like a global variable — use sparingly

// ✅ Better: inject dependencies instead of using static access
class ApiClient {
final String baseUrl; // instance field — injectable, testable
ApiClient(this.baseUrl);
}

// ✅ Static constants are safe — they can't be mutated
class Durations {
Durations._();
static const Duration short = Duration(milliseconds: 200);
static const Duration medium = Duration(milliseconds: 500);
static const Duration long = Duration(seconds: 2);
static const Duration network = Duration(seconds: 30);
}

// ✅ Static methods with no side effects are safe
class StringUtils {
StringUtils._();
static String capitalize(String s) =>
s.isEmpty ? s : s[0].toUpperCase() + s.substring(1);
static bool isEmail(String s) => s.contains('@') && s.contains('.');
static String slugify(String s) =>
s.toLowerCase().replaceAll(RegExp(r'\s+'), '-')
.replaceAll(RegExp(r'[^a-z0-9-]'), '');
}

Static Members in Inheritance

// Static members are NOT inherited polymorphically
class Animal {
static String type = 'animal';
static Animal create() => Animal();

void describe() => print('I am a ${Animal.type}');
}

class Dog extends Animal {
static String type = 'dog'; // shadows, not overrides
static Dog create() => Dog(); // shadows Animal.create()


void describe() => print('I am a ${Dog.type}'); // uses own static
}

Animal a = Dog();
print(Animal.type); // animal — static dispatch, not dynamic
print(Dog.type); // dog
// a.type // ❌ compile error — can't call static on instance

Static Initializers

Static fields can be initialized with expressions — they run lazily, the first time the field is accessed:

class RegExPatterns {
RegExPatterns._();

// These are computed once on first access
static final RegExp email = RegExp(r'^[\w.-]+@[\w.-]+\.\w{2,}$');
static final RegExp phone = RegExp(r'^\+?[\d\s\-()]{7,15}$');
static final RegExp url = RegExp(r'https?://\S+');
static final RegExp integer = RegExp(r'^-?\d+$');
static final RegExp decimal = RegExp(r'^-?\d+(\.\d+)?$');

static bool isEmail(String s) => email.hasMatch(s.trim());
static bool isPhone(String s) => phone.hasMatch(s.trim());
static bool isInteger(String s) => integer.hasMatch(s.trim());
}

// RegExp objects are compiled on first call to isEmail() etc., then cached
print(RegExPatterns.isEmail('alice@example.com')); // true
print(RegExPatterns.isPhone('+1 555 0100')); // true

Summary

MemberSyntaxShared?Access
Static fieldstatic T name = value;✅ All instancesClass.name
Static constantstatic const T name = value;✅ Compile-timeClass.name
Static methodstatic R method(params) {}✅ No thisClass.method()
Static getterstatic T get name => ...;Class.name
Static factorystatic T create() => T();Class.create()
Instance fieldT name;❌ Per objectobj.name
Instance methodR method() {}❌ Has thisobj.method()

Best practices:

  • Static constants — always fine, use freely
  • Static utility methods (no side effects) — fine
  • Static mutable state — treat like global variables, use sparingly
  • Prefer dependency injection over static access for testability