Classes in Dart
last modified May 30, 2026
In this article we will explore classes in Dart, a powerful feature of the Dart programming language that enables object-oriented programming (OOP).
Dart Classes Overview
Classes are at the core of object-oriented programming in Dart. They act as blueprints for creating objects that encapsulate data and behavior. This tutorial comprehensively covers working with Dart classes, from basic class definitions to advanced concepts such as mixins, extensions, and abstract classes.
By leveraging Dart's class system, developers can write clean, reusable, and modular code. The ability to implement encapsulation, inheritance, and polymorphism makes Dart's class structure an essential tool for designing scalable applications. Modern Dart 3 builds on this foundation with sealed classes, class modifiers, enhanced enums, and pattern matching for safer, more expressive API design.
| Feature | Description | Example |
|---|---|---|
| Class Definition | Blueprint for creating objects | class Point { ... } |
| Constructors | Methods for initializing objects | Point({required this.x, required this.y}); |
| Properties | Variables within a class | double x, y; |
| Methods | Functions within a class | void move() { ... } |
| Inheritance | Enables creation of subclasses | class Vector extends Point {} |
| Mixins | Allows sharing behaviors across classes | class Robot with Walker {} |
| Abstract Classes | Defines structure without implementation | abstract class Shape { ... } |
| Extensions | Adds functionality to existing classes | extension on String { ... } |
| Field-formal Parameters | Shorthand for assigning constructor args to fields | User(this.name, this.age); |
| Getters & Setters | Computed and validated property access | double get fahrenheit => ... |
| Static Members | Class-level fields and methods | static int count = 0; |
| Enums | Named constants with optional fields and methods | enum Status { success(200); ... } |
| Equality & hashCode | Value-based comparison for objects | bool operator ==(Object o) => ... |
| Class Modifiers | Control subclassing with final/base/sealed/interface | sealed class Result<T> {} |
A class in Dart is a user-defined type that combines state (properties) and behavior (methods). It allows developers to model real-world entities, define relationships between objects, and enforce structured programming.
Basic Class Definition
A class in Dart is defined using the class keyword followed by the
class name. The body of the class contains its properties (data) and methods
(behavior). Here's a simple example of a class representing a point in 2D space:
class Point {
final double x;
final double y;
const Point({required this.x, required this.y});
Point copyWith({double? x, double? y}) =>
Point(x: x ?? this.x, y: y ?? this.y);
@override
String toString() => 'Point($x, $y)';
}
void main() {
const p = Point(x: 3.0, y: 4.0);
print(p);
final moved = p.copyWith(x: p.x + 1.0, y: p.y - 1.0);
print('New position: $moved');
}
The Point class uses final fields and a
const constructor, making instances immutable. Named parameters
with required prevent accidental argument swaps. The
copyWith method is a common Dart pattern for producing modified
copies of immutable objects without mutating the original.
In main, the const keyword on the instance signals
that the value is a compile-time constant, enabling compiler optimisations and
sharing of identical instances. The copyWith call produces a new
Point with updated coordinates while the original remains
unchanged.
$ dart run basic_class.dart Point(3.0, 4.0) New position: Point(4.0, 3.0)
Field-formal Parameters
A field-formal parameter (written this.fieldName)
is a constructor parameter that automatically assigns its value to the
corresponding instance field. It eliminates the boilerplate of writing
this.x = x in the constructor body.
class User {
final String name;
int age;
String role;
// Positional field-formal parameters
User(this.name, this.age, [this.role = 'guest']);
// Named field-formal parameters — all required except role
User.named({required this.name, required this.age, this.role = 'guest'});
@override
String toString() => 'User($name, $age, $role)';
}
void main() {
var u1 = User('Alice', 30);
var u2 = User('Bob', 25, 'admin');
var u3 = User.named(name: 'Carol', age: 28, role: 'editor');
print(u1); // User(Alice, 30, guest)
print(u2); // User(Bob, 25, admin)
print(u3); // User(Carol, 28, editor)
}
Field-formal parameters work with both positional and named constructors. When
the field is final, the parameter can only assign to it — mutation
must be done through a setter or is not possible at all. The optional positional
parameter [this.role = 'guest'] shows that default values work
exactly the same way as with regular parameters. Named constructors can mix
positional and field-formal parameters freely.
$ dart run field_formal.dart User(Alice, 30, guest) User(Bob, 25, admin) User(Carol, 28, editor)
Constructors and Initialization
Dart offers several ways to initialize class instances. Constructors can be simple or complex, with options for named parameters, initializer lists, and factory constructors. Here's a more advanced example demonstrating these features:
class Rectangle {
final double width;
final double height;
final String color;
const Rectangle({
required this.width,
required this.height,
this.color = 'black',
}) : assert(width > 0),
assert(height > 0);
Rectangle.square(double size, {String color = 'black'})
: this(width: size, height: size, color: color);
Rectangle.fromJson(Map<String, dynamic> json)
: width = (json['width'] as num).toDouble(),
height = (json['height'] as num).toDouble(),
color = (json['color'] as String?) ?? 'black';
factory Rectangle.fromString(String input) {
final parts = input.split('x');
return Rectangle(
width: double.parse(parts[0]),
height: double.parse(parts[1]),
);
}
double get area => width * height;
}
void main() {
var rect1 = Rectangle(width: 10.0, height: 20.0);
var rect2 = Rectangle.square(15.0, color: 'red');
var rect3 = Rectangle.fromJson({'width': 5.0, 'height': 8.0});
var rect4 = Rectangle.fromString('12x24');
print('Area of rect1: ${rect1.area}');
print('Area of rect2: ${rect2.area}');
print('Area of rect3: ${rect3.area}');
print('Area of rect4: ${rect4.area}');
}
The main const constructor uses named parameters with
required for width and height, and an
optional color with a default. The assert calls
enforce that dimensions are positive at both debug and test time.
Rectangle.square is a redirecting named constructor that delegates
to the primary constructor with identical width and height.
fromJson uses an initializer list with explicit type casts for
robustness. The factory constructor fromString parses a dimension
string such as 12x24 and returns a validated instance. The
area getter demonstrates a computed property.
$ dart run constructors.dart Area of rect1: 200.0 Area of rect2: 225.0 Area of rect3: 40.0 Area of rect4: 288.0
Getters, Setters, and Computed Properties
Dart supports getter and setter accessors, which provide controlled access to private fields and allow computed properties that look like regular fields to callers.
class Temperature {
double _celsius;
Temperature(this._celsius);
// Getter: read access to private field
double get celsius => _celsius;
// Setter: validated write access
set celsius(double value) {
if (value < -273.15) throw ArgumentError('Below absolute zero');
_celsius = value;
}
// Computed getter: converts on the fly, no extra field needed
double get fahrenheit => _celsius * 1.8 + 32;
// Setter: accepts Fahrenheit and converts to internal Celsius
set fahrenheit(double f) => _celsius = (f - 32) / 1.8;
@override
String toString() =>
'${_celsius.toStringAsFixed(1)}°C / ${fahrenheit.toStringAsFixed(1)}°F';
}
void main() {
var t = Temperature(100);
print(t); // 100.0°C / 212.0°F
t.fahrenheit = 32;
print(t); // 0.0°C / 32.0°F
t.celsius = 37;
print(t); // 37.0°C / 98.6°F
}
The private field _celsius is the single source of truth.
The celsius setter enforces a physical invariant (no temperature
below absolute zero) before storing the value. The fahrenheit
getter computes its value on every access rather than storing a redundant field,
keeping the class in a consistent state automatically. Callers interact with
t.fahrenheit as if it were a plain property — the conversion is
invisible.
$ dart run getters_setters.dart 100.0°C / 212.0°F 0.0°C / 32.0°F 37.0°C / 98.6°F
Inheritance and Polymorphism
Dart supports single inheritance where a class can extend one other class. The following example demonstrates inheritance, method overriding, and polymorphism:
import 'dart:math';
abstract class Shape {
final String color;
const Shape(this.color);
double get area;
void describe() =>
print('$runtimeType(color: $color, area: ${area.toStringAsFixed(2)})');
}
class Circle extends Shape {
final double radius;
const Circle(String color, this.radius) : super(color);
@override
double get area => pi * radius * radius;
}
class Square extends Shape {
final double side;
const Square(String color, this.side) : super(color);
@override
double get area => side * side;
}
void main() {
const shapes = [
Circle('red', 5.0),
Square('blue', 4.0),
];
for (final shape in shapes) {
shape.describe();
}
}
Marking Shape as abstract prevents direct
instantiation and declares area as an abstract getter that every
subclass must implement. The const constructor propagates to
subclasses, enabling compile-time constant instances. Using
dart:math's pi constant gives precise area
calculations. The toStringAsFixed call in describe
formats the output to two decimal places for readability. In main,
the list itself is a compile-time constant because every element is
const.
$ dart run inheritance.dart Circle(color: red, area: 78.54) Square(color: blue, area: 16.00)
Static Members
Static members belong to the class itself rather than to any instance. They are accessed through the class name and are shared across all instances.
class Counter {
// Static field: shared across all instances
static int _count = 0;
// Static constant
static const int maxCount = 100;
// Instance field set from the static counter
final int id;
Counter() : id = ++_count {
if (_count > maxCount) throw StateError('Counter limit reached');
}
// Static getter
static int get count => _count;
// Static method
static void reset() => _count = 0;
@override
String toString() => 'Counter($id)';
}
void main() {
var a = Counter();
var b = Counter();
var c = Counter();
print(Counter.count); // 3
print(a); // Counter(1)
print(b); // Counter(2)
Counter.reset();
print(Counter.count); // 0
}
_count is shared state — every Counter() call reads
and increments the same variable. Static constants like maxCount
are compile-time values and can be used in const expressions.
Static methods cannot access this because there is no instance in
scope. They are useful for utility functions and factory-style logic that does
not depend on object state.
$ dart run static_members.dart 3 Counter(1) Counter(2) 0
Mixins and Composition
Dart uses mixins to share code across multiple class hierarchies. A mixin is a way to reuse a class's code in multiple class hierarchies without using inheritance. Here's an example demonstrating mixins and composition:
mixin Logger {
void log(String message) =>
print('[${DateTime.now()}] $message');
}
mixin JsonSerializable {
String toJsonString() {
return '{"type": "$runtimeType"}';
}
}
class Person with Logger {
final String name;
int age;
Person(this.name, this.age);
void celebrateBirthday() {
age++;
log('$name is now $age years old');
}
}
class Product with Logger, JsonSerializable {
final String id;
final double price;
Product(this.id, this.price);
void applyDiscount(double percent) {
final discount = price * (percent / 100);
log('Applying $percent% discount (\$$discount) to product $id');
}
}
void main() {
var person = Person('Alice', 30);
person.celebrateBirthday();
var product = Product('12345', 99.99);
product.applyDiscount(10);
print(product.toJsonString());
}
The Logger mixin adds timestamped logging to any class without
inheritance, using bracket-delimited timestamps for readability. The
JsonSerializable mixin uses runtimeType to produce a
type-aware JSON stub. Person keeps name immutable but
allows age to be mutated — declaring age as
final would cause a compile error on age++.
Product mixes in both behaviours, demonstrating that a single class
can compose multiple mixins alongside its own state.
$ dart run mixins.dart
[2025-05-25 14:30:45.123456] Alice is now 31 years old
[2025-05-25 14:30:45.123456] Applying 10% discount ($9.999) to product 12345
{"type": "Product"}
Enhanced Enums
Dart enums are more powerful than in most languages: they can carry fields, methods, and even implement interfaces. Introduced in Dart 2.17, enhanced enums turn what would otherwise be a class into a concise named constant.
enum HttpStatus {
ok(200, 'OK'),
created(201, 'Created'),
notFound(404, 'Not Found'),
serverError(500, 'Internal Server Error');
final int code;
final String message;
const HttpStatus(this.code, this.message);
bool get isSuccess => code < 400;
bool get isError => code >= 400;
@override
String toString() => '$code $message';
}
void handleStatus(HttpStatus status) {
final description = switch (status) {
HttpStatus.ok || HttpStatus.created => 'Request succeeded',
HttpStatus() when status.isError => 'Request failed: $status',
};
print(description);
}
void main() {
print(HttpStatus.ok); // 200 OK
print(HttpStatus.notFound); // 404 Not Found
print(HttpStatus.ok.isSuccess); // true
handleStatus(HttpStatus.created);
handleStatus(HttpStatus.serverError);
// Iterate over all values
for (final s in HttpStatus.values) {
print('\${s.name}: \${s.code}');
}
}
Each enum constant is a compile-time constant with its own code
and message values. The const constructor is required
for enhanced enums. The computed getters isSuccess and
isError add behaviour without extra storage. The switch
expression uses an || pattern to match multiple values in one arm
and a when guard to test the property. HttpStatus.values
returns the ordered list of all constants, which is useful for iteration and
serialisation.
$ dart run enhanced_enums.dart 200 OK 404 Not Found true Request succeeded Request failed: 500 Internal Server Error ok: 200 created: 201 notFound: 404 serverError: 500
Advanced Class Features
Dart provides several advanced class features including abstract classes, interfaces, extension methods, and operator overloading. The following example demonstrates these concepts:
// Sealed class (Dart 3): all subtypes must be declared in the same library
sealed class Animal {
String get name;
void makeSound();
}
// Every class is an implicit interface in Dart
class Bird {
void fly() => print('Flying high');
}
// Extending a sealed class and implementing an interface
class Parrot extends Animal implements Bird {
@override
final String name;
Parrot(this.name);
@override
void makeSound() => print('$name says: Squawk!');
@override
void fly() => print('$name is flying in circles');
// Operator overloading
Parrot operator +(Parrot other) => Parrot('$name & ${other.name}');
}
// Extension method adds behaviour without modifying the source class
extension on Parrot {
void repeat(String phrase) =>
print('$name repeats: $phrase $phrase $phrase');
}
// Exhaustive switch expression enabled by the sealed class
String describeAnimal(Animal a) => switch (a) {
Parrot(name: var n) => 'A parrot named $n',
};
void main() {
var polly = Parrot('Polly');
var matey = Parrot('Matey');
polly.makeSound();
polly.fly();
var couple = polly + matey;
print('New parrot: ${couple.name}');
polly.repeat('Hello');
print(describeAnimal(polly));
}
Declaring Animal as sealed restricts subclassing to
the same library, giving the compiler a complete picture of all possible
subtypes. This makes switch expressions on Animal
exhaustively checkable — if you add a new subtype and forget to handle it, the
compiler reports a warning. The describeAnimal function uses a
switch expression with Dart 3 object-pattern syntax
(Parrot(name: var n)) to destructure the matched value inline.
Operator overloading and extension methods remain available on concrete classes
regardless of whether the hierarchy is sealed.
$ dart run advanced_features.dart Polly says: Squawk! Polly is flying in circles New parrot: Polly & Matey Polly repeats: Hello Hello Hello A parrot named Polly
Equality and hashCode
By default, Dart compares objects by identity (i.e. same
memory address). To compare objects by value, you must
override operator == and hashCode. These two must
always be overridden together: objects that are equal must produce the same
hash code.
class Color {
final int r;
final int g;
final int b;
const Color(this.r, this.g, this.b);
@override
bool operator ==(Object other) =>
other is Color && other.r == r && other.g == g && other.b == b;
@override
int get hashCode => Object.hash(r, g, b);
@override
String toString() => 'Color($r, $g, $b)';
}
void main() {
final red1 = Color(255, 0, 0);
final red2 = Color(255, 0, 0);
final blue = Color(0, 0, 255);
print(red1 == red2); // true (same values)
print(red1 == blue); // false
print(identical(red1, red2)); // false (different instances)
// Correct hashCode makes Sets and Maps work as expected
final palette = {red1, red2, blue};
print(palette.length); // 2
}
Object.hash (Dart 2.14+) combines multiple values into a
well-distributed hash code, eliminating the need for manual XOR arithmetic.
The identical check confirms that red1 and
red2 are distinct objects even though they compare equal. A
Set uses both == and hashCode: because
both colours are equal, the set deduplicates them to a single entry. Always
override both together — inconsistent implementations cause silent bugs in
collections.
$ dart run equality.dart true false false 2
Class Modifiers
Dart 3 introduced class modifiers that let library authors precisely control how their classes may be used outside the library. Without a modifier, a class can be freely extended, implemented, and mixed in.
| Modifier | Can extend | Can implement | Can use as mixin | Purpose |
|---|---|---|---|---|
final |
✗ | ✗ | ✗ | Closed type — no outside subtyping |
base |
✓ | ✗ | ✗ | Allow extension, forbid implement |
interface |
✗ | ✓ | ✗ | Allow implement, forbid extend |
sealed |
same lib only | same lib only | ✗ | Exhaustive switching; closed hierarchy |
The table shows how each modifier restricts class usage. The final
modifier prevents all outside subclassing, while base and
interface allow specific subtyping patterns. sealed
classes are the most powerful: they enforce exhaustive switching and restrict
subtypes to the same library.
// sealed: all subtypes must live in the same library
sealed class Result<T> {}
class Success<T> extends Result<T> {
final T value;
Success(this.value);
}
class Failure<T> extends Result<T> {
final String error;
Failure(this.error);
}
// Exhaustive: the compiler knows Success and Failure are all subtypes
String handle(Result<int> result) => switch (result) {
Success(value: var v) => 'Got $v',
Failure(error: var e) => 'Error: $e',
};
// interface: callers implement the contract, cannot extend
interface class Printable {
void printInfo();
}
// final: prevent all outside subclassing
final class ImmutableConfig {
final String host;
final int port;
const ImmutableConfig({required this.host, required this.port});
}
void main() {
print(handle(Success(42))); // Got 42
print(handle(Failure('timeout'))); // Error: timeout
final cfg = ImmutableConfig(host: 'localhost', port: 8080);
print('\${cfg.host}:\${cfg.port}'); // localhost:8080
}
sealed is the most powerful modifier for domain modelling: because
the compiler sees every subtype, a switch expression on a sealed
type is statically exhaustiveness-checked. final locks a class
completely — useful for value types and configuration objects where uncontrolled
subclassing could break invariants. interface communicates that a
class is purely a contract: callers must provide a full implementation rather
than inheriting one. base is the inverse — subclasses may extend
but must honour the base class's invariants.
$ dart run class_modifiers.dart Got 42 Error: timeout localhost:8080
Source
Dart Language: Classes
Dart Language: Constructors
Dart Language: Mixins
Dart Language: Extensions
Dart Language: Enums
Dart Language: Class Modifiers
Dart Language: Patterns
Classes are fundamental to Dart's object-oriented programming model. They provide encapsulation of data and behaviour, support inheritance and polymorphism through class hierarchies, and enable code reuse through mixins. Dart 3 extends this with sealed classes, class modifiers, enhanced enums, and exhaustive pattern matching — features that bring compile-time safety and expressive API design to both application and library code. Mastering Dart's class system, from basic field-formal parameters and computed properties to immutable value objects and closed hierarchies, is essential for writing robust Dart and Flutter applications.
Author
List all Dart tutorials.