Dart Object-Oriented Programming
OOP for Flutter and Beyond
Explanation
OOP in Dart
Dart is a pure object-oriented language where everything is an object. It supports classes, mixins, abstract classes, and interfaces to build robust applications.
Key Concepts
| Concept | Dart Implementation | |---------|---------------------| | Classes | class keyword | | Inheritance | extends | | Interfaces | implicit (all classes) | | Mixins | with keyword | | Encapsulation | _ prefix for private |
Demonstration
Example 1: Classes and Objects
// Basic class
class User {
// Instance fields
String name;
String email;
int _age; // Private (underscore prefix)
// Static field
static int userCount = 0;
// Constant
static const String defaultRole = 'user';
// Constructor
User(this.name, this.email, [this._age = 0]) {
userCount++;
}
// Named constructor
User.guest() : name = 'Guest', email = 'guest@example.com', _age = 0 {
userCount++;
}
// Factory constructor
factory User.fromJson(Map<String, dynamic> json) {
return User(
json['name'] as String,
json['email'] as String,
json['age'] as int? ?? 0,
);
}
// Getter
int get age => _age;
// Setter with validation
set age(int value) {
if (value < 0) throw ArgumentError('Age cannot be negative');
_age = value;
}
// Computed property
String get displayName => '$name <$email>';
// Instance method
String greet() => 'Hello, I\'m $name!';
// Static method
static int getUserCount() => userCount;
// toString override
@override
String toString() => 'User{name: $name, email: $email, age: $_age}';
// Equality override
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is User &&
runtimeType == other.runtimeType &&
email == other.email;
@override
int get hashCode => email.hashCode;
}
// Usage
void main() {
final user1 = User('Arthur', 'art@bpc.com', 30);
final user2 = User.guest();
final user3 = User.fromJson({'name': 'Sarah', 'email': 'sarah@example.com'});
print(user1.greet());
print(User.getUserCount()); // 3
// Cascade notation
final user = User('Test', 'test@example.com')
..age = 25
..name = 'Test User';
}
Example 2: Inheritance
// Base class
abstract class Animal {
final String name;
int _age = 0;
Animal(this.name);
// Abstract method
String speak();
// Concrete method
String describe() => '${runtimeType}: $name';
// Getter
int get age => _age;
// Protected-like (accessible in subclasses)
void incrementAge() => _age++;
}
// Subclass
class Dog extends Animal {
final String breed;
Dog(String name, this.breed) : super(name);
@override
String speak() => '$name says Woof!';
String fetch() => '$name is fetching the ball';
}
class Cat extends Animal {
final bool indoor;
Cat(String name, {this.indoor = true}) : super(name);
@override
String speak() => '$name says Meow!';
String scratch() => '$name is scratching';
}
// Polymorphism
void main() {
final animals = <Animal>[
Dog('Buddy', 'Golden Retriever'),
Cat('Whiskers', indoor: true),
Dog('Max', 'German Shepherd'),
];
for (final animal in animals) {
print(animal.speak()); // Polymorphic call
print(animal.describe());
// Type checking
if (animal is Dog) {
print(animal.fetch()); // Smart cast
}
}
}
Example 3: Mixins
// Mixin definition
mixin Flyable {
double altitude = 0;
void fly() {
altitude += 100;
print('Flying at $altitude meters');
}
void land() {
altitude = 0;
print('Landed');
}
}
mixin Swimmable {
double depth = 0;
void swim() {
depth += 10;
print('Swimming at $depth meters deep');
}
void surface() {
depth = 0;
print('Surfaced');
}
}
// Mixin with constraints
mixin Trainable on Animal {
bool trained = false;
void train() {
trained = true;
print('$name is now trained');
}
}
// Using mixins
class Bird extends Animal with Flyable {
Bird(String name) : super(name);
@override
String speak() => '$name chirps';
}
class Duck extends Animal with Flyable, Swimmable {
Duck(String name) : super(name);
@override
String speak() => '$name quacks';
}
class TrainedDog extends Animal with Trainable {
TrainedDog(String name) : super(name);
@override
String speak() => '$name barks';
}
void main() {
final duck = Duck('Donald');
duck.fly(); // From Flyable
duck.swim(); // From Swimmable
final dog = TrainedDog('Buddy');
dog.train(); // From Trainable
}
Example 4: Abstract Classes and Interfaces
// Abstract class
abstract class Shape {
double get area;
double get perimeter;
void draw();
// Concrete method
String describe() => '${runtimeType} with area $area';
}
// Interface (implicit - every class is an interface)
class Drawable {
void draw() {
print('Drawing...');
}
}
class Clickable {
void onClick() {}
}
// Implementing multiple interfaces
class Circle extends Shape implements Drawable, Clickable {
final double radius;
Circle(this.radius);
@override
double get area => 3.14159 * radius * radius;
@override
double get perimeter => 2 * 3.14159 * radius;
@override
void draw() {
print('Drawing circle with radius $radius');
}
@override
void onClick() {
print('Circle clicked');
}
}
class Rectangle extends Shape {
final double width;
final double height;
Rectangle(this.width, this.height);
@override
double get area => width * height;
@override
double get perimeter => 2 * (width + height);
@override
void draw() {
print('Drawing rectangle ${width}x$height');
}
}
// Using implements for interface-like behavior
abstract class Repository<T> {
Future<T?> findById(int id);
Future<List<T>> findAll();
Future<T> save(T entity);
Future<void> delete(int id);
}
class UserRepository implements Repository<User> {
final List<User> _storage = [];
@override
Future<User?> findById(int id) async {
return _storage.firstWhereOrNull((u) => u.id == id);
}
@override
Future<List<User>> findAll() async => List.unmodifiable(_storage);
@override
Future<User> save(User entity) async {
_storage.add(entity);
return entity;
}
@override
Future<void> delete(int id) async {
_storage.removeWhere((u) => u.id == id);
}
}
Example 5: Generics
// Generic class
class Box<T> {
T? _content;
void put(T item) => _content = item;
T? get() => _content;
bool get isEmpty => _content == null;
}
// Generic with constraints
class NumberBox<T extends num> {
T value;
NumberBox(this.value);
double toDouble() => value.toDouble();
T add(T other) => (value + other) as T;
}
// Generic methods
T firstOrNull<T>(List<T> list) {
return list.isEmpty ? null as T : list.first;
}
T max<T extends Comparable<T>>(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// Generic interface
abstract class Cache<K, V> {
V? get(K key);
void set(K key, V value);
void remove(K key);
void clear();
}
class InMemoryCache<K, V> implements Cache<K, V> {
final Map<K, V> _storage = {};
@override
V? get(K key) => _storage[key];
@override
void set(K key, V value) => _storage[key] = value;
@override
void remove(K key) => _storage.remove(key);
@override
void clear() => _storage.clear();
}
void main() {
final stringBox = Box<String>();
stringBox.put('Hello');
print(stringBox.get());
final numberBox = NumberBox<int>(42);
print(numberBox.toDouble());
final cache = InMemoryCache<String, int>();
cache.set('count', 100);
}
Example 6: Extension Methods
// Extension on existing class
extension StringExtensions on String {
String capitalize() {
if (isEmpty) return this;
return '${this[0].toUpperCase()}${substring(1)}';
}
String truncate(int length, {String suffix = '...'}) {
if (this.length <= length) return this;
return '${substring(0, length)}$suffix';
}
bool get isEmail => contains('@') && contains('.');
}
extension ListExtensions<T> on List<T> {
T? firstWhereOrNull(bool Function(T) test) {
for (final element in this) {
if (test(element)) return element;
}
return null;
}
List<T> distinctBy<K>(K Function(T) keyOf) {
final seen = <K>{};
return where((element) => seen.add(keyOf(element))).toList();
}
}
extension DateTimeExtensions on DateTime {
String toIso8601DateString() {
return '${year.toString().padLeft(4, '0')}-'
'${month.toString().padLeft(2, '0')}-'
'${day.toString().padLeft(2, '0')}';
}
bool isSameDay(DateTime other) {
return year == other.year &&
month == other.month &&
day == other.day;
}
}
// Named extension for organization
extension NumericIterable on Iterable<num> {
num get sum => fold(0, (a, b) => a + b);
double get average => isEmpty ? 0 : sum / length;
}
void main() {
print('hello'.capitalize()); // Hello
print('Hello World'.truncate(5)); // Hello...
print('test@example.com'.isEmail); // true
final users = [User('A'), User('B'), User('A')];
final distinct = users.distinctBy((u) => u.name);
final numbers = [1, 2, 3, 4, 5];
print(numbers.sum); // 15
print(numbers.average); // 3.0
}
Key Takeaways:
- Everything in Dart is an object
- Use mixins for code reuse
- Interfaces are implicit
- Extension methods add functionality
- Generics provide type safety
Imitation
Challenge 1: Build a Task System
Task: Create a task management system with different task types and states.
Solution
// Task status enum
enum TaskStatus { pending, inProgress, completed, cancelled }
// Base task class
abstract class Task {
final String id;
String title;
String description;
TaskStatus _status;
final DateTime createdAt;
DateTime? completedAt;
Task({
required this.title,
this.description = '',
}) : id = DateTime.now().millisecondsSinceEpoch.toString(),
_status = TaskStatus.pending,
createdAt = DateTime.now();
TaskStatus get status => _status;
int get priority;
void start() {
if (_status != TaskStatus.pending) {
throw StateError('Task already started');
}
_status = TaskStatus.inProgress;
}
void complete() {
if (_status != TaskStatus.inProgress) {
throw StateError('Task not in progress');
}
_status = TaskStatus.completed;
completedAt = DateTime.now();
}
void cancel() {
if (_status == TaskStatus.completed) {
throw StateError('Cannot cancel completed task');
}
_status = TaskStatus.cancelled;
}
}
// Specific task types
class BugTask extends Task {
final String severity;
BugTask({
required String title,
String description = '',
required this.severity,
}) : super(title: title, description: description);
@override
int get priority {
switch (severity) {
case 'critical': return 1;
case 'high': return 2;
case 'medium': return 3;
default: return 4;
}
}
}
class FeatureTask extends Task {
final int storyPoints;
FeatureTask({
required String title,
String description = '',
required this.storyPoints,
}) : super(title: title, description: description);
@override
int get priority => storyPoints > 8 ? 2 : 3;
}
// Task manager
class TaskManager {
final List<Task> _tasks = [];
void addTask(Task task) => _tasks.add(task);
List<Task> getByStatus(TaskStatus status) {
return _tasks
.where((t) => t.status == status)
.toList()
..sort((a, b) => a.priority.compareTo(b.priority));
}
Task? findById(String id) {
return _tasks.firstWhereOrNull((t) => t.id == id);
}
}
void main() {
final manager = TaskManager();
manager.addTask(BugTask(
title: 'Fix login',
severity: 'critical',
));
manager.addTask(FeatureTask(
title: 'Add dark mode',
storyPoints: 5,
));
for (final task in manager.getByStatus(TaskStatus.pending)) {
print('${task.title} - Priority: ${task.priority}');
}
}
Practice
Exercise 1: Implement Observable Pattern
Difficulty: Intermediate
Create an observable class:
- Subscribe/unsubscribe listeners
- Notify on changes
- Type-safe events
Exercise 2: Build a State Machine
Difficulty: Advanced
Create a generic state machine:
- Define states and transitions
- Guards for transitions
- Event hooks
Summary
What you learned:
- Dart class fundamentals
- Inheritance and polymorphism
- Mixins for code reuse
- Extension methods
- Generics for type safety
Next Steps:
- Read: Dart API
- Practice: Build a Flutter widget
- Explore: Dart isolates
