Fields
Understand how to define and use fields (instance variables) in Dart classes.
What is it?
Fields are variables that hold data within a class. They represent the state of an object and can be instance fields (unique to each object), static fields (shared across all instances), or class-level constants.
Why does it exist?
Fields exist to:
- Store object state and data
- Represent the properties of a class
- Enable encapsulation of data
- Provide access to object attributes
- Support data persistence and manipulation
Instance Fields
Basic Instance Fields
class Person {
// Instance fields (each object has its own copy)
String name;
int age;
bool isActive;
// Constructor
Person(this.name, this.age, this.isActive);
// Another way
Person.withDefaults(String name)
: name = name,
age = 0,
isActive = true;
}
// Usage
var person1 = Person('Alice', 25, true);
var person2 = Person('Bob', 30, false);
// Each instance has its own values
print(person1.name); // Alice
print(person2.name); // Bob
Final Fields
class User {
// Final fields (set once, cannot be changed)
final String id;
final String name;
final DateTime createdAt;
// Optional final field
final String? email;
User(this.id, this.name, this.email)
: createdAt = DateTime.now();
// Named constructor
User.fromEmail(String email, String name)
: id = email.hashCode.toString(),
name = name,
email = email,
createdAt = DateTime.now();
}
// Usage
var user = User('123', 'Alice', 'alice@example.com');
print(user.id); // 123
print(user.name); // Alice
// Cannot modify final fields
// user.name = 'Bob'; // Error: Cannot assign to final
Late Fields
class DatabaseService {
// Late fields (initialized later)
late String _connectionString;
late String _databaseName;
// Initialized in constructor
DatabaseService(String connectionString)
: _connectionString = connectionString;
// Late initialization with method
void initialize(String dbName) {
_databaseName = dbName;
}
// Late final (assigned once)
late final String apiKey;
void setApiKey(String key) {
apiKey = key; // Can only be called once
// apiKey = 'newkey'; // Error: Already assigned
}
void connect() {
// Late fields must be initialized before use
print('Connecting to $_databaseName with $_connectionString');
}
}
// Usage
var db = DatabaseService('mysql://localhost');
db.initialize('my_db');
db.setApiKey('secret123');
db.connect(); // Connecting to my_db with mysql://localhost
Static Fields
Class-Level Variables
class AppConfig {
// Static fields (shared across all instances)
static String appName = 'My App';
static String version = '1.0.0';
static bool isDebug = false;
// Static final
static final DateTime startTime = DateTime.now();
// Static constant
static const int maxRetries = 3;
static const String defaultLocale = 'en_US';
// Private static field
static int _instanceCount = 0;
// Static getter
static int get instanceCount => _instanceCount;
// Static setter
static set debugMode(bool value) {
isDebug = value;
}
AppConfig() {
_instanceCount++;
}
}
// Usage (no instance needed)
print(AppConfig.appName); // My App
print(AppConfig.version); // 1.0.0
print(AppConfig.maxRetries); // 3
AppConfig.debugMode = true;
print(AppConfig.isDebug); // true
// Instance count
var config1 = AppConfig();
var config2 = AppConfig();
print(AppConfig.instanceCount); // 2
Private Fields
Encapsulation with Private Fields
class BankAccount {
// Private fields (starts with _)
String _accountNumber;
double _balance = 0;
String _accountType;
// Public fields
final String accountHolder;
BankAccount(this.accountHolder, this._accountNumber, this._accountType);
// Public getters
String get accountNumber => _accountNumber;
double get balance => _balance;
String get accountType => _accountType;
// Public methods to modify private data
void deposit(double amount) {
if (amount > 0) {
_balance += amount;
_logTransaction('deposit', amount);
}
}
bool withdraw(double amount) {
if (amount > 0 && amount <= _balance) {
_balance -= amount;
_logTransaction('withdraw', amount);
return true;
}
return false;
}
void changeAccountNumber(String newNumber) {
if (_validateAccountNumber(newNumber)) {
_accountNumber = newNumber;
}
}
// Private method
void _logTransaction(String type, double amount) {
print('$type of \$$amount for $_accountNumber');
}
// Private validation
bool _validateAccountNumber(String number) {
return number.length == 10 && RegExp(r'^\d+$').hasMatch(number);
}
}
// Usage
var account = BankAccount('Alice', '1234567890', 'checking');
account.deposit(1000);
account.withdraw(500);
// Cannot access private fields
// print(account._balance); // Error: Private field
// account._logTransaction(); // Error: Private method
// Can access through getters
print(account.balance); // 500
Computed Fields (Getters)
Calculated Properties
class Rectangle {
// Private fields
double _width;
double _height;
Rectangle(this._width, this._height);
// Computed getters (no backing field)
double get area => _width * _height;
double get perimeter => 2 * (_width + _height);
double get diagonal => math.sqrt(_width * _width + _height * _height);
// Boolean getter
bool get isSquare => _width == _height;
// String getter
String get description => 'Rectangle(${_width}x$_height)';
// Setters with validation
set width(double value) {
if (value > 0) {
_width = value;
}
}
set height(double value) {
if (value > 0) {
_height = value;
}
}
// Computed setter
set size(double value) {
_width = value;
_height = value;
}
}
// Usage
var rect = Rectangle(10, 20);
print(rect.area); // 200
print(rect.perimeter); // 60
print(rect.isSquare); // false
print(rect.description); // Rectangle(10x20)
rect.size = 15;
print(rect.isSquare); // true
print(rect.description); // Rectangle(15x15)
Static vs Instance Fields
When to Use Each
class Vehicle {
// Instance fields (unique to each object)
final String model;
final String licensePlate;
String color;
// Static fields (shared across all instances)
static int _totalVehicles = 0;
static const int maxSpeed = 200;
static String manufacturer = 'Generic Motors';
// Static method to get total
static int get totalVehicles => _totalVehicles;
Vehicle(this.model, this.licensePlate, this.color) {
_totalVehicles++;
}
// Instance method uses both
String get description => '$model by $manufacturer (${color})';
// Static method utility
static bool isValidLicensePlate(String plate) {
return plate.length == 7 && plate.contains('-');
}
}
// Usage
var car1 = Vehicle('Sedan', 'ABC-123', 'red');
var car2 = Vehicle('SUV', 'XYZ-789', 'blue');
print(car1.model); // Sedan (instance)
print(Vehicle.manufacturer); // Generic Motors (static)
print(Vehicle.maxSpeed); // 200 (static constant)
print(Vehicle.totalVehicles); // 2 (static)
print(Vehicle.isValidLicensePlate('ABC-123')); // true (static)
Initialization Order
Field Initialization Sequence
class InitializationDemo {
// Fields initialized in declaration order
int a = 10;
int b = 20;
// Can reference previously declared fields
int c = a + b; // 30
// Static fields initialized first
static int staticField = 100;
// Late fields
late int lateField;
// Constructor initializer list
InitializationDemo(int value)
: lateField = value,
a = value * 2 {
// Constructor body runs last
print('Constructor body');
}
// Static initializer
static int staticComputed = _computeStatic();
static int _computeStatic() {
print('Static initialization');
return 42;
}
}
// Usage
var demo = InitializationDemo(5);
print(demo.a); // 10 (or 10 from initializer)
print(demo.c); // 30
print(demo.lateField); // 5
print(InitializationDemo.staticField); // 100
Best Practices
Use Private Fields for Encapsulation
// Good: Encapsulated with private fields
class Person {
String _name;
int _age;
Person(this._name, this._age);
String get name => _name;
int get age => _age;
void celebrateBirthday() {
_age++;
}
set name(String value) {
if (value.isNotEmpty) {
_name = value;
}
}
}
// Bad: Exposed fields
class BadPerson {
String name;
int age;
BadPerson(this.name, this.age);
// Anyone can modify without validation
}
Use Final When Possible
// Good: Immutable fields
class ImmutableUser {
final String id;
final String name;
final DateTime createdAt;
ImmutableUser(this.id, this.name, this.createdAt);
}
// Bad: Mutable when not needed
class MutableUser {
String id;
String name;
DateTime createdAt;
MutableUser(this.id, this.name, this.createdAt);
}
Use Static for Shared Data
// Good: Shared configuration
class AppSettings {
static String theme = 'light';
static String language = 'en';
static bool notifications = true;
static void setTheme(String theme) {
AppSettings.theme = theme;
}
}
// Bad: Duplicated data
class UserSettings {
String theme = 'light';
String language = 'en';
}
Common Mistakes
Using Static for Instance Data
Wrong:
class Person {
static String name; // Wrong! Name should be instance-specific
Person(this.name); // Error: Can't use this with static
}
Correct:
class Person {
String name; // Instance field
Person(this.name);
}
Late Field Not Initialized
Wrong:
class Service {
late String _config;
void useService() {
print(_config); // Error: Late field not initialized
}
}
Correct:
class Service {
late String _config;
void initialize(String config) {
_config = config; // Must be called before use
}
void useService() {
print(_config); // Safe if initialized
}
}
Summary
Fields store the state of objects and classes. Understanding the different types (instance, static, final, late, private) and when to use them is crucial for effective object-oriented programming in Dart.
Next Steps
Now that you understand fields, continue to:
Did You Know?
- Fields are initialized in declaration order
- Final fields must be initialized in constructor
- Late fields are initialized lazily
- Private fields are only accessible within the class
- Static fields are initialized when the class is loaded
- Getters can compute values without backing fields
- Setter parameters can be validated before assignment