Object-oriented programming in Dart is the foundation of every Flutter application. Widgets, state objects, models, services — they are all classes. A solid understanding of how Dart classes work unlocks the ability to design clean, maintainable application architectures.
What Is a Class?
A class is a blueprint that defines:
- Properties (attributes) — data the object holds
- Methods — actions the object can perform
- Constructors — how to create an instance of the object
- Access control — what is public and what is private
An object (also called an instance) is a specific realization of a class, with its own set of property values.
Class: Person (blueprint)
--> Properties: name, age
--> Methods: greet(), birthday()
Object: person1 = Person('Amr', 30)
Object: person2 = Person('Sara', 25)
Defining a Basic Class
class Person {
// Public properties
String name;
int age;
// Private property (prefixed with _)
String _id;
// Constructor
Person(this.name, this.age, this._id);
// Method
void introduce() {
print('Hi, I am $name and I am $age years old.');
}
}
void main() {
var person1 = Person('Amr', 30, 'usr_001');
person1.introduce(); // Hi, I am Amr and I am 30 years old.
print(person1.name); // Amr
print(person1.age); // 30
// print(person1._id); // ERROR: _id is private (only accessible within same file)
}
Constructors
Default Constructor
Dart provides a concise shorthand for assigning constructor parameters directly to instance fields using this.:
class Point {
double x;
double y;
// Shorthand: this.x and this.y assign the parameters to fields automatically
Point(this.x, this.y);
}
var p = Point(3.0, 4.0);
print(p.x); // 3.0
Named Constructors
Dart allows multiple constructors on the same class using named constructors. Useful for creating objects in different ways.
class Color {
int red;
int green;
int blue;
Color(this.red, this.green, this.blue);
// Named constructor for creating from a hex string
Color.fromHex(String hex)
: red = int.parse(hex.substring(0, 2), radix: 16),
green = int.parse(hex.substring(2, 4), radix: 16),
blue = int.parse(hex.substring(4, 6), radix: 16);
// Named constructor for black
Color.black() : red = 0, green = 0, blue = 0;
}
var white = Color(255, 255, 255);
var blue = Color.fromHex('0000FF');
var black = Color.black();
Constructor with Default Parameter Values
class User {
String name;
int age;
String role;
User(this.name, [this.age = 18, this.role = 'viewer']);
}
var user1 = User('Amr');
var user2 = User('Sara', 25, 'admin');
Getters and Setters
Getters compute or format a value from existing properties without exposing internal representation. Setters validate input before assigning a value.
class Circle {
double _radius;
Circle(this._radius);
// Getter — computed property
double get area => 3.14159 * _radius * _radius;
double get circumference => 2 * 3.14159 * _radius;
double get radius => _radius;
// Setter — validates before assigning
set radius(double value) {
if (value < 0) {
throw ArgumentError('Radius cannot be negative');
}
_radius = value;
}
}
void main() {
var c = Circle(5.0);
print(c.area); // 78.539...
print(c.circumference); // 31.415...
c.radius = 10.0; // Uses the setter
// c.radius = -1; // Throws ArgumentError
}
Static Members
Static properties and methods belong to the class itself, not to any instance. They can be called without creating an object.
class MathUtils {
static const double pi = 3.14159265358979;
static int square(int x) => x * x;
static double circleArea(double radius) => pi * radius * radius;
static int max(int a, int b) => a > b ? a : b;
}
void main() {
print(MathUtils.pi); // 3.14159...
print(MathUtils.square(5)); // 25
print(MathUtils.circleArea(3)); // 28.274...
print(MathUtils.max(10, 7)); // 10
}
Static members are useful for utility functions and constants that logically belong to a class but do not depend on instance state.
Encapsulation
Encapsulation means hiding internal implementation details and exposing only what is necessary. In Dart, members are made private by prefixing with _ (underscore).
class BankAccount {
final String _accountNumber;
double _balance;
BankAccount(this._accountNumber, double initialBalance)
: _balance = initialBalance;
double get balance => _balance; // Read-only access to balance
void deposit(double amount) {
if (amount <= 0) throw ArgumentError('Deposit must be positive');
_balance += amount;
}
bool withdraw(double amount) {
if (amount > _balance) return false; // Insufficient funds
_balance -= amount;
return true;
}
}
void main() {
var account = BankAccount('ACC-001', 1000.0);
account.deposit(500.0);
print(account.balance); // 1500.0
print(account.withdraw(200.0)); // true
// account._balance = 0; // ERROR: private member
}
Inheritance
Inheritance allows a class to extend another class, inheriting all its properties and methods. The child class can override parent methods or add new ones.
class Animal {
String name;
int age;
Animal(this.name, this.age);
void eat() {
print('$name is eating');
}
void sleep() {
print('$name is sleeping');
}
@override
String toString() => 'Animal($name, $age)';
}
class Dog extends Animal {
String breed;
Dog(String name, int age, this.breed) : super(name, age);
void bark() {
print('$name says: Woof!');
}
// Override the parent's eat method
@override
void eat() {
print('$name eats dog food enthusiastically');
}
}
class Cat extends Animal {
Cat(String name, int age) : super(name, age);
void purr() {
print('$name purrs softly');
}
}
void main() {
var dog = Dog('Rex', 3, 'Labrador');
dog.eat(); // Rex eats dog food enthusiastically (overridden)
dog.bark(); // Rex says: Woof!
dog.sleep(); // Rex is sleeping (inherited)
var cat = Cat('Whiskers', 2);
cat.eat(); // Whiskers is eating (from Animal)
cat.purr(); // Whiskers purrs softly
}
Abstract Classes
An abstract class defines a contract — it declares methods that subclasses must implement, but does not provide the implementation itself.
abstract class Shape {
double get area;
double get perimeter;
void describe() {
print('Area: $area, Perimeter: $perimeter');
}
}
class Rectangle extends Shape {
double width;
double height;
Rectangle(this.width, this.height);
@override
double get area => width * height;
@override
double get perimeter => 2 * (width + height);
}
class Circle extends Shape {
double radius;
Circle(this.radius);
@override
double get area => 3.14159 * radius * radius;
@override
double get perimeter => 2 * 3.14159 * radius;
}
Interfaces (via implements)
In Dart, every class implicitly defines an interface. Use implements to enforce that a class provides a specific set of methods.
abstract class Serializable {
Map<String, dynamic> toJson();
String toJsonString();
}
class Product implements Serializable {
final int id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
@override
Map<String, dynamic> toJson() => {'id': id, 'name': name, 'price': price};
@override
String toJsonString() => toJson().toString();
}
OOP Principles in Dart
| Principle | Dart Feature |
|---|---|
| Encapsulation | _ prefix for private members, getters/setters |
| Inheritance | extends keyword, super calls |
| Polymorphism | Method overriding with @override |
| Abstraction | abstract classes, implements |
These principles work together to produce code that is modular, testable, and easy to reason about — the foundation of every well-structured Flutter app.