đ Lesson 7: Advanced OOP Features
Beyond Basic OOP: Four Power Features
đ§ These features make Dart uniquely powerful for building complex applications
You already know classes, inheritance, and polymorphism. Now learn the advanced tools that professional Dart developers use every day.
Enhanced Enums
Enums with properties, methods, constructors, and even interfaces â far beyond simple named values.
Extensions
Add new methods and getters to any existing class â even classes you don't own or can't modify.
Mixins
Compose classes from reusable pieces of behavior. Like multiple inheritance, but safe and controlled.
Factory Constructors
Create objects conditionally â return cached instances, subtypes, or even null from a constructor.
Enhanced Enums â Enums with Superpowers
đ§ Before Dart 2.17, enums were just named constants. Now they can have properties, methods, and constructors.
This transforms enums from simple labels into full-featured types that can carry data and logic.
đ´ Old Enums (Pre-2.17)
enum LogLevel {
debug, info, warning, error
}
// That's it â just names.
// Need severity? Use a switch.
// Need icons? Use a separate map.
đ° Data and logic scattered across switch statements and maps
⨠Enhanced Enums (Dart 2.17+)
enum LogLevel {
debug(0, 'đ'),
info(1, 'âšī¸'),
warning(2, 'â ī¸'),
error(3, 'â');
final int severity;
final String icon;
const LogLevel(this.severity, this.icon);
bool get isError => severity >= 3;
}
đ Self-contained: data, logic, and values all in one place
Complete Enhanced Enum with Line-by-Line Explanation
enum HttpStatus {
// Values with constructor arguments
ok(200, 'OK'),
notFound(404, 'Not Found'),
serverError(500, 'Internal Server Error');
// Properties (one per value)
final int code;
final String message;
// Constructor (must be const)
const HttpStatus(this.code, this.message);
// Computed property â logic inside the enum!
bool get isSuccess => code >= 200 && code < 300;
// Static method to look up by code
static HttpStatus? fromCode(int code) {
return values.cast<HttpStatus?>().firstWhere(
(s) => s?.code == code,
orElse: () => null,
);
}
}
đ Breakdown
enum declares an enhanced enumfinal requiredconstvalues is a built-in list of all enum valuesđī¸ Build Your Own Enhanced Enum
đ Generated Code
enum HttpStatus {
ok(200, 'OK'),
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 >= 200 && code < 300;
bool get isError => code >= 400;
static HttpStatus? fromCode(int code) {
return values.cast<HttpStatus?>().firstWhere(
(s) => s?.code == code,
orElse: () => null,
);
}
}
Extensions â Adding Functionality to Existing Types
đ§ Extensions let you add methods to any class â even built-in types like String, int, or List
You don't need to subclass or modify the original class. Define an extension and use the new methods as if they were always there. This is perfect for adding utility methods without cluttering the original API.
Complete Extension Example with Line-by-Line Explanation
// Define an extension on the String class
extension StringExtensions on String {
// Capitalize the first letter
String get capitalize {
if (isEmpty) return this;
return this[0].toUpperCase() + substring(1);
}
// Check if the string is a valid email
bool get isValidEmail {
return contains('@') && contains('.');
}
// Convert "Hello World" â "hello-world"
String get toSlug {
return toLowerCase()
.replaceAll(RegExp(r'\s+'), '-')
.replaceAll(RegExp(r'[^a-z0-9-]'), '');
}
}
// Usage â these look like built-in methods!
void main() {
print('hello'.capitalize); // Hello
print('a@b.c'.isValidEmail); // true
print('Hello World!'.toSlug); // hello-world
}
đ Breakdown
extension ... on String adds to String()get creates a computed propertybool'hello'.capitalize â clean syntax!Real-World Extension Use Cases
đ DateTime Extensions
extension DateHelpers on DateTime {
String get timeAgo {
final diff = DateTime.now().difference(this);
if (diff.inDays > 365) return '\${(diff.inDays/365).floor()}y ago';
if (diff.inDays > 0) return '\${diff.inDays}d ago';
if (diff.inHours > 0) return '\${diff.inHours}h ago';
return 'just now';
}
}
đĸ Number Formatting
extension NumberFormat on num {
String get toFileSize {
if (this < 1024) return '\$this B';
if (this < 1048576) return '\${(this/1024).toStringAsFixed(1)} KB';
return '\${(this/1048576).toStringAsFixed(1)} MB';
}
}
đ List Utilities
extension ListUtils<T> on List<T> {
T? secondOrNull() => length > 1 ? this[1] : null;
bool get isNotEmptyAndNotNull => isNotEmpty;
}
âī¸ Try It: Test String Extensions
Mixins â Composing Classes from Reusable Behaviors
đ§ A mixin is a class that provides methods to other classes â without being a parent class
Unlike inheritance (which creates an "is-a" relationship), mixins provide a "has-capability" relationship. Use the with keyword to mix in one or more mixins. A single class can use multiple mixins, composing behavior from many sources.
âĸ Inheritance (
extends): A Dog is an Animal. Single parent only.âĸ Mixins (
with): A Dog has logging capability, validation capability. Multiple mixins allowed.âĸ Mixins cannot be instantiated on their own â they only work when mixed into a class.
Defining and Using Mixins
// Define mixins (use 'mixin' keyword instead of 'class')
mixin Logger {
void log(String message) => print('[LOG] \$message');
void error(String message) => print('[ERROR] \$message');
}
mixin Validator {
bool isValidEmail(String email) => email.contains('@');
bool isNotEmpty(String? value) => value != null && value.isNotEmpty;
}
// Use mixins with the 'with' keyword
class UserService with Logger, Validator {
void createUser(String email) {
if (!isValidEmail(email)) {
error('Invalid email: \$email');
return;
}
log('User created with email: \$email');
}
}
void main() {
var service = UserService();
service.createUser('test@example.com'); // [LOG] User created...
service.createUser('bad-email'); // [ERROR] Invalid email...
}
đ§Ŧ Available Mixins
đī¸ Your Composed Class
class UserService {
// Click mixins to add capabilities
}
đŗ Class Hierarchy
Factory Constructors â Flexible Object Creation
đ§ A factory constructor doesn't always create a new instance
Unlike regular constructors, factory constructors can return an existing cached instance, a subtype, or even null. Use them when object creation needs logic beyond simple field assignment.
âĸ Caching/Singletons: Return the same instance every time
âĸ Subtype selection: Return different subclasses based on parameters
âĸ Validation: Check conditions before creating the object
âĸ Async initialization: Factory can return a Future (rare)
Factory Constructor Patterns
class User {
final String name;
final String role;
final List<String> permissions;
// Private constructor â only factories can call it
User._(this.name, this.role, this.permissions);
// Factory 1: Create admin with full permissions
factory User.admin(String name) {
return User._(name, 'Admin', ['read', 'write', 'delete']);
}
// Factory 2: Create regular user with limited permissions
factory User.regular(String name) {
return User._(name, 'User', ['read', 'write']);
}
// Factory 3: Create guest with read-only access
factory User.guest() {
return User._('Guest', 'Guest', ['read']);
}
}
// Usage â each factory creates a differently configured User
void main() {
var admin = User.admin('Alice'); // Admin with full permissions
var user = User.regular('Bob'); // Regular user
var guest = User.guest(); // Guest â no name needed
print(admin.permissions); // [read, write, delete]
print(guest.permissions); // [read]
}
đŽ Simulate Factory Creation
đ¯ Advanced OOP Quiz
Score: 0/5 | Hints: 3
Loading question...
Practice Projects
Apply what you've learned by building these hands-on projects.