Answer
Overview
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects — which bundle data (properties) and behavior (methods) together. Dart is fully object-oriented — everything is an object, even numbers and functions.
OOP has 4 core pillars:
1. Encapsulation
Hiding the internal state of an object and only exposing what is necessary through a public interface.
dartclass BankAccount { // Private field — hidden from outside (only accessible within this file) double _balance = 0; // Public getter — controlled read access double get balance => _balance; // Public methods — controlled modification void deposit(double amount) { if (amount > 0) _balance += amount; } void withdraw(double amount) { if (amount > 0 && amount <= _balance) _balance -= amount; } } void main() { final account = BankAccount(); account.deposit(500); print(account.balance); // 500.0 // account._balance = 9999; // ❌ Error — private field }
Key Idea: Use
prefix in Dart to make fields/methods private. Note: private in Dart means library-private (file-level), not class-private.text_
2. Inheritance (Detailed)
A class (child/subclass) can inherit properties and methods from another class (parent/superclass), enabling code reuse.
Dart provides 4 ways to achieve inheritance-like behavior:
2.1 textextends
— True Inheritance ("is-a" relationship)
extendsThe child class inherits everything from the parent — fields, methods, and constructors are chained.
dartclass Animal { String name; Animal(this.name) { print('Animal constructor: $name'); } void eat() => print('$name is eating'); void speak() => print('...'); } class Dog extends Animal { String breed; Dog(String name, this.breed) : super(name) { print('Dog constructor: $name, $breed'); } void speak() => print('$name says Woof!'); // eat() is inherited — no need to override } void main() { final dog = Dog('Buddy', 'Labrador'); // Output: // Animal constructor: Buddy ← Parent constructor called FIRST // Dog constructor: Buddy, Labrador ← Then child constructor dog.eat(); // Buddy is eating ← Inherited from Animal dog.speak(); // Buddy says Woof! ← Overridden in Dog }
Key behaviors of extends
- Parent constructor is called automatically (via )text
super - Child inherits all methods and fields
- Child only needs to methods it wants to changetext
@override - Single inheritance only — one class maxtext
extends
2.2 textimplements
— Interface Contract ("can-do" relationship)
implementsThe class promises to provide the same API but must write every method from scratch. No code is inherited.
dartclass Printer { String brand; Printer(this.brand) { print('Printer constructor: $brand'); } void printDoc(String doc) => print('Printing: $doc on $brand'); void scan() => print('Scanning on $brand'); } class SmartPrinter implements Printer { String brand; // ✅ Must declare field — not inherited SmartPrinter(this.brand) { print('SmartPrinter constructor: $brand'); } void printDoc(String doc) => print('Smart printing: $doc'); // ✅ Must override void scan() => print('Smart scanning'); // ✅ Must override — even though Printer has a body } void main() { final sp = SmartPrinter('HP'); // Output: // SmartPrinter constructor: HP // ❌ "Printer constructor" is NEVER printed — implements does NOT call parent constructor sp.printDoc('Resume'); // Smart printing: Resume }
Key behaviors of implements
- Parent constructor is NOT called
- Nothing is inherited — you must override ALL methods and declare ALL fields
- Multiple implements allowed — text
implements A, B, C - The parent class is treated as an interface (API contract only)
2.3 textwith
— Mixin ("has-ability" relationship)
withMixins let you reuse code from multiple sources without inheritance hierarchy. Think of them as "plug-in abilities".
dartmixin Flyable { void fly() => print('Flying!'); } mixin Swimmable { void swim() => print('Swimming!'); } mixin Runnable { void run() => print('Running!'); } // Duck can fly, swim, and run — all abilities mixed in class Duck with Flyable, Swimmable, Runnable { String name; Duck(this.name); } // Fish can only swim class Fish with Swimmable { String name; Fish(this.name); } void main() { final duck = Duck('Donald'); duck.fly(); // Flying! ← From Flyable mixin duck.swim(); // Swimming! ← From Swimmable mixin duck.run(); // Running! ← From Runnable mixin final fish = Fish('Nemo'); fish.swim(); // Swimming! // fish.fly(); // ❌ Error — Fish doesn't have Flyable mixin }
Key behaviors of with
- Code is reused (methods are inherited, not just the contract)
- Multiple mixins allowed — text
with A, B, C - No constructor in mixins (use keyword, nottext
mixin)textclass - Can combine with —text
extendstextclass Duck extends Bird with Flyable, Swimmable
2.4 textabstract class
— Cannot Be Instantiated
abstract classAn abstract class defines a blueprint — some methods can have bodies, others are abstract (no body).
dartabstract class PaymentGateway { // Abstract — no body, MUST be implemented by child Future<bool> processPayment(double amount); void refund(double amount); // Concrete — has body, child inherits this for FREE void showReceipt(double amount) { print('Receipt: ₹$amount processed successfully'); } } class RazorpayGateway extends PaymentGateway { Future<bool> processPayment(double amount) async { print('Processing ₹$amount via Razorpay'); return true; } void refund(double amount) => print('Refunding ₹$amount via Razorpay'); // showReceipt() inherited for free — no override needed } void main() { // PaymentGateway(); // ❌ Error — cannot instantiate abstract class final gateway = RazorpayGateway(); gateway.processPayment(500); // Processing ₹500 via Razorpay gateway.showReceipt(500); // Receipt: ₹500 processed successfully }
Constructor Behavior — The Critical Difference
This is what most interviews test. Understanding constructor chaining is essential.
dartclass A { A() { print('Constructor A'); } void method() => print('Method from A'); } class B extends A { B() { print('Constructor B'); } } class C implements A { C() { print('Constructor C'); } void method() => print('Method from C'); // Must override } class D extends A { D() { print('Constructor D'); } // method() inherited — no override needed } void main() { print('--- extends ---'); B b = B(); // Output: // Constructor A ← Parent called first // Constructor B print('--- implements ---'); C c = C(); // Output: // Constructor C ← ONLY child constructor, A's constructor NOT called print('--- extends (no override) ---'); D d = D(); d.method(); // Method from A ← Inherited without override }
When to Choose Which — Decision Guide
Use textextends
when:
extendsdart// ✅ "is-a" relationship — Dog IS an Animal class Dog extends Animal {} // ✅ You want to REUSE parent code (constructors + methods) class AdminUser extends User { // Inherits login(), profile, etc. void deleteUser(String id) {} // Extra admin ability } // ✅ You only need to override SOME methods class PremiumButton extends ElevatedButton { // Inherits all button behavior, just customize style }
Use textimplements
when:
implementsdart// ✅ "can-do" contract — guarantee API compatibility class MockUserRepository implements UserRepository { // For testing — write completely different implementation Future<User> getUser(String id) async => User(id: id, name: 'Mock'); } // ✅ Multiple interface compliance class SmartDevice implements Printable, Scannable, Faxable { void print_() {} void scan() {} void fax() {} } // ✅ You want NO parent code — clean implementation from scratch class CustomList implements List<int> { // Must implement EVERY List method yourself }
Use textwith
(mixin) when:
withdart// ✅ Sharing abilities across unrelated classes mixin Logger { void log(String msg) => print('[LOG] $msg'); } mixin Cacheable { final Map<String, dynamic> _cache = {}; void cache(String key, dynamic value) => _cache[key] = value; dynamic getCache(String key) => _cache[key]; } // UserService and ProductService are unrelated // but both need logging and caching class UserService with Logger, Cacheable { void fetchUser() { log('Fetching user'); cache('user', {'name': 'Alice'}); } } class ProductService with Logger, Cacheable { void fetchProducts() { log('Fetching products'); cache('products', []); } }
Use textabstract class
when:
abstract classdart// ✅ Define a contract with SOME shared code abstract class BaseRepository { // Shared implementation — all repos get this void logQuery(String query) => print('[DB] $query'); // Abstract — each repo must implement Future<List<Map>> fetchAll(); Future<void> insert(Map data); } class UserRepository extends BaseRepository { Future<List<Map>> fetchAll() async { logQuery('SELECT * FROM users'); // Inherited return []; } Future<void> insert(Map data) async { logQuery('INSERT INTO users'); } }
Combining extends + implements + with
Dart allows all three together in a single class:
dartabstract class Animal { String name; Animal(this.name); void eat() => print('$name eating'); void breathe() => print('$name breathing'); } abstract class Pet { void play(); void cuddle(); } mixin Trainable { void train(String command) => print('Learning: $command'); } mixin Vaccinated { bool isVaccinated = false; void vaccinate() { isVaccinated = true; print('Vaccinated!'); } } // Dog IS an Animal, CAN-DO Pet things, HAS Trainable + Vaccinated abilities class Dog extends Animal with Trainable, Vaccinated implements Pet { Dog(String name) : super(name); void play() => print('$name playing fetch'); void cuddle() => print('$name cuddling'); } void main() { final dog = Dog('Buddy'); dog.eat(); // Buddy eating ← from extends Animal dog.breathe(); // Buddy breathing ← from extends Animal dog.play(); // Buddy playing fetch ← from implements Pet dog.cuddle(); // Buddy cuddling ← from implements Pet dog.train('sit'); // Learning: sit ← from with Trainable dog.vaccinate(); // Vaccinated! ← from with Vaccinated }
extends vs implements vs with — Master Comparison
| Feature | text | text | text |
|---|---|---|---|
| Relationship | "is-a" | "can-do" (contract) | "has-ability" |
| Parent constructor called? | Yes (via text | No | No constructor allowed |
| Inherits method bodies? | Yes | No (must override ALL) | Yes |
| Inherits fields? | Yes | No (must redeclare) | Yes |
| Must override methods? | Only abstract ones | ALL methods | Only abstract ones |
| Multiple allowed? | No (single only) | Yes ( text | Yes ( text |
| Can have constructor? | Yes | Yes (own only) | No |
| Code reuse? | Full | None (contract only) | Full |
| Real-world analogy | Child inherits from parent | Employee signs a job contract | Person learns a new skill |
3. Polymorphism
The ability for different classes to be treated as instances of the same parent class, while behaving differently.
dartabstract class Shape { double area(); // Abstract — must be implemented by subclasses } class Circle extends Shape { double radius; Circle(this.radius); double area() => 3.14159 * radius * radius; } class Rectangle extends Shape { double width, height; Rectangle(this.width, this.height); double area() => width * height; } void printArea(Shape shape) { // Same method call — different behavior based on actual type print('Area: ${shape.area()}'); } void main() { printArea(Circle(5)); // Area: 78.54 printArea(Rectangle(4, 6)); // Area: 24.0 // Same function, different results — that's polymorphism }
Two types:
- Compile-time (Method Overloading) — not directly supported in Dart
- Runtime (Method Overriding) — fully supported via
text@override
4. Abstraction
Hiding complex implementation details and only showing the essential features. Achieved via abstract classes and interfaces.
dart// User of PaymentGateway doesn't care HOW payment works internally abstract class PaymentGateway { Future<bool> processPayment(double amount); void showReceipt(); } // Different implementations — user doesn't need to know the details class RazorpayGateway extends PaymentGateway { Future<bool> processPayment(double amount) async { // Complex Razorpay SDK logic hidden behind simple interface print('Processing ₹$amount via Razorpay'); return true; } void showReceipt() => print('Razorpay Receipt'); } class StripeGateway extends PaymentGateway { Future<bool> processPayment(double amount) async { print('Processing \$$amount via Stripe'); return true; } void showReceipt() => print('Stripe Receipt'); } // Usage — doesn't matter which gateway, same API void checkout(PaymentGateway gateway, double amount) async { final success = await gateway.processPayment(amount); if (success) gateway.showReceipt(); }
4 Pillars Summary
| Pillar | Concept | Dart Feature | Real-World Analogy |
|---|---|---|---|
| Encapsulation | Hide internal data | text | ATM hides cash mechanism, exposes buttons |
| Inheritance | Reuse parent class | text text text | Child inherits traits from parents |
| Polymorphism | Same interface, different behavior | text | Same "drive" action — car vs bike vs truck |
| Abstraction | Show only essentials | text | TV remote hides circuit board, shows buttons |
Dart 3.0+ Class Modifiers
Dart 3.0 introduced new keywords to control how classes can be used:
dart// abstract — cannot instantiate, can extend/implement abstract class Shape { } // interface — can ONLY be implemented, not extended interface class Printable { void print_() => print('Printing'); } // class A extends Printable {} // ❌ Error class A implements Printable { // ✅ OK void print_() => print('Custom print'); } // final — cannot be extended OR implemented outside its library final class Config { final String apiUrl; Config(this.apiUrl); } // class MyConfig extends Config {} // ❌ Error outside library // class MyConfig implements Config {} // ❌ Error outside library // sealed — like abstract + final, used for exhaustive pattern matching sealed class Result {} class Success extends Result { final String data; Success(this.data); } class Failure extends Result { final String error; Failure(this.error); } // Exhaustive switch — compiler ensures all cases covered String handleResult(Result result) => switch (result) { Success(data: var d) => 'Got: $d', Failure(error: var e) => 'Error: $e', // No default needed — compiler knows all subtypes }; // base — can be extended but not implemented outside its library base class BaseService { void init() => print('Service initialized'); }
Class Modifiers Comparison
| Modifier | Can instantiate? | Can text | Can text | Can use outside library? |
|---|---|---|---|---|
| (none) | Yes | Yes | Yes | Yes |
text | No | Yes | Yes | Yes |
text | Yes | No | Yes | Yes |
text | Yes | No | No | Yes (use only) |
text | No | Yes (same library) | No | Yes (use only) |
text | Yes | Yes | No | Yes |
5. Getters & Setters
Getters and setters provide controlled access to an object's fields. They are a core tool for encapsulation.
dartclass Temperature { double _celsius; // Private backing field Temperature(this._celsius); // Getter — read access with logic double get celsius => _celsius; double get fahrenheit => (_celsius * 9 / 5) + 32; // Computed property String get status { if (_celsius > 37) return 'Fever'; if (_celsius < 35) return 'Hypothermia'; return 'Normal'; } // Setter — write access with validation set celsius(double value) { if (value < -273.15) { throw ArgumentError('Temperature cannot be below absolute zero'); } _celsius = value; } set fahrenheit(double value) { celsius = (value - 32) * 5 / 9; // Uses the setter with validation } } void main() { final temp = Temperature(36.6); // Getter usage — looks like a field, but runs logic print(temp.celsius); // 36.6 print(temp.fahrenheit); // 97.88 print(temp.status); // Normal // Setter usage — looks like assignment, but runs validation temp.celsius = 38.5; print(temp.status); // Fever temp.fahrenheit = 98.6; // Sets via conversion print(temp.celsius); // 37.0 // temp.celsius = -300; // ❌ Throws ArgumentError }
Getter-Only (Read-Only Property)
dartclass Circle { final double radius; Circle(this.radius); // Read-only computed properties — no setter double get area => 3.14159 * radius * radius; double get circumference => 2 * 3.14159 * radius; // circle.area = 50; // ❌ Error — no setter defined }
Rule: Use getters for computed/derived values. Use setters for validated writes. If a field needs no logic, just use a regular
field.textfinal
6. Constructor Types
Dart supports 5 types of constructors — each serves a different purpose.
6.1 Default Constructor
dartclass User { String name; int age; // Default constructor with shorthand User(this.name, this.age); } final user = User('Alice', 25);
6.2 Named Constructor
Create multiple constructors with different names for different use cases.
dartclass User { String name; int age; String role; User(this.name, this.age, this.role); // Named constructors — different ways to create a User User.guest() : name = 'Guest', age = 0, role = 'guest'; User.admin(String name) : this.name = name, age = 0, role = 'admin'; User.fromJson(Map<String, dynamic> json) : name = json['name'], age = json['age'], role = json['role'] ?? 'user'; } void main() { final user1 = User('Alice', 25, 'user'); final user2 = User.guest(); final user3 = User.admin('Bob'); final user4 = User.fromJson({'name': 'Charlie', 'age': 30}); }
6.3 Factory Constructor
A constructor that doesn't always create a new instance. Can return cached instances, subtypes, or null.
dartclass Database { static Database? _instance; // Cached singleton final String connectionString; // Private constructor — cannot be called from outside Database._internal(this.connectionString); // Factory — returns existing instance if available factory Database(String connStr) { _instance ??= Database._internal(connStr); return _instance!; } } void main() { final db1 = Database('mysql://localhost'); final db2 = Database('postgres://localhost'); // Returns same instance! print(identical(db1, db2)); // true — same object (singleton) }
Factory can also return subtypes:
dartabstract class Shape { factory Shape(String type) { switch (type) { case 'circle': return Circle(); case 'square': return Square(); default: throw ArgumentError('Unknown shape: $type'); } } double area(); } class Circle implements Shape { double area() => 3.14 * 5 * 5; } class Square implements Shape { double area() => 10 * 10; } void main() { Shape s = Shape('circle'); // Returns Circle instance print(s.area()); // 78.5 }
6.4 Const Constructor
Creates compile-time constants — identical const objects share the same memory.
dartclass Point { final double x; final double y; const Point(this.x, this.y); // All fields must be final } void main() { const p1 = Point(1, 2); const p2 = Point(1, 2); print(identical(p1, p2)); // true — same object in memory! // Non-const — different objects final p3 = Point(1, 2); final p4 = Point(1, 2); print(identical(p3, p4)); // false — different objects }
Flutter uses const extensively —
is cached and reused, improving performance.textconst Text('Hello')
6.5 Redirecting Constructor
One constructor delegates to another constructor in the same class.
dartclass User { String name; int age; String role; User(this.name, this.age, this.role); // Redirecting — delegates to the main constructor User.defaultUser() : this('Guest', 0, 'guest'); User.admin(String name) : this(name, 0, 'admin'); }
Constructor Types Comparison
| Constructor | Purpose | Returns New Instance? | Example |
|---|---|---|---|
| Default | Standard creation | Always | text |
| Named | Multiple creation patterns | Always | text |
| Factory | Conditional creation, singletons | Not always | text |
| Const | Compile-time constants | Shared if identical | text |
| Redirecting | Delegate to another constructor | Always | text |
7. textsuper
and textthis
Keywords
superthistextthis
— Reference to Current Instance
thisdartclass User { String name; int age; // this.name refers to the FIELD, name refers to the PARAMETER User(String name, int age) { this.name = name; // Disambiguate field vs parameter this.age = age; } // Shorthand — same thing // User(this.name, this.age); void printInfo() { print('Name: ${this.name}'); // this is optional here print('Name: $name'); // Same thing — no ambiguity } // Returning this for method chaining (Builder pattern) User setName(String name) { this.name = name; return this; // Return current instance for chaining } User setAge(int age) { this.age = age; return this; } } void main() { // Method chaining using this final user = User('', 0) .setName('Alice') .setAge(25); print(user.name); // Alice }
textsuper
— Reference to Parent Class
superdartclass Animal { String name; Animal(this.name); void makeSound() => print('$name makes a sound'); void eat() => print('$name is eating'); } class Dog extends Animal { String breed; // super() — call parent constructor Dog(String name, this.breed) : super(name); // super.method() — call parent's version of a method void makeSound() { super.makeSound(); // Call parent's makeSound first print('$name says Woof!'); // Then add own behavior } void eat() { print('$name eats dog food'); // Completely replace parent behavior // super.eat() NOT called — parent's eat() is ignored } } void main() { final dog = Dog('Buddy', 'Lab'); dog.makeSound(); // Output: // Buddy makes a sound ← super.makeSound() // Buddy says Woof! ← own behavior added dog.eat(); // Output: // Buddy eats dog food ← parent's eat() NOT called }
Dart 3.0+ textsuper
Parameters
superdart// Old way class Dog extends Animal { String breed; Dog(String name, this.breed) : super(name); } // Dart 3.0+ — super parameters (shorter) class Dog extends Animal { String breed; Dog(super.name, this.breed); // super.name passes directly to parent }
8. Method Overriding Rules
Method overriding lets a child class provide its own implementation of a parent method.
Rules
dartclass Parent { void greet() => print('Hello from Parent'); int calculate(int x) => x * 2; void process(num value) => print('Processing: $value'); } class Child extends Parent { // Rule 1: Same method name // Rule 2: Same or compatible return type (can be narrower) // Rule 3: Same parameters // Rule 4: Use @override annotation (recommended, not required) void greet() => print('Hello from Child'); // ✅ Same signature int calculate(int x) => x * 3; // ✅ Same return type // ❌ Cannot change parameter type (without covariant) // void process(int value) {} // Error — num ≠ int }
@override Annotation — Why Use It?
dartclass Parent { void greet() => print('Hello'); } class Child extends Parent { // Without @override — works but risky void greet() => print('Hi'); // ⚠️ No compile-time check // With @override — compiler verifies parent has this method void greet() => print('Hi'); // ✅ Compiler confirms Parent.greet() exists // If parent renames greet() to sayHello(): // Without @override → silent bug (Child.greet never called polymorphically) // With @override → compile error (catches the problem immediately) }
Always use
— it catches bugs when parent methods are renamed or removed.text@override
textcovariant
— Override Parameter Types
covariantdartclass Animal { void chase(Animal other) => print('Chasing animal'); } class Dog extends Animal { // ❌ Without covariant: // void chase(Dog other) {} // Error — Dog is not Animal // ✅ With covariant — tells Dart "I guarantee only Dogs will be passed" void chase(covariant Dog other) => print('Dog chasing dog: ${other}'); }
9. Operator Overloading
Dart lets you redefine operators (
+-==[]dartclass Vector { final double x; final double y; const Vector(this.x, this.y); // Overload + operator Vector operator +(Vector other) { return Vector(x + other.x, y + other.y); } // Overload - operator Vector operator -(Vector other) { return Vector(x - other.x, y - other.y); } // Overload * operator (scalar multiplication) Vector operator *(double scalar) { return Vector(x * scalar, y * scalar); } // Overload == operator bool operator ==(Object other) { if (other is! Vector) return false; return x == other.x && y == other.y; } int get hashCode => Object.hash(x, y); // Overload [] operator (index access) double operator [](int index) { if (index == 0) return x; if (index == 1) return y; throw RangeError('Index must be 0 or 1'); } String toString() => 'Vector($x, $y)'; } void main() { const a = Vector(1, 2); const b = Vector(3, 4); print(a + b); // Vector(4.0, 6.0) ← + operator print(a - b); // Vector(-2.0, -2.0) ← - operator print(a * 3); // Vector(3.0, 6.0) ← * operator print(a == b); // false ← == operator print(a[0]); // 1.0 ← [] operator }
Overloadable Operators in Dart
| Operator | Example | Purpose |
|---|---|---|
text text text text | text | Arithmetic |
text | text | Equality (always override text |
text text text text | text | Comparison |
text text | text text | Index access |
text | text | Bitwise NOT |
text | text | Negation |
Important: Always override
when you overridetexthashCode. Dart uses both intext==,textMap, and collections.textSet
10. Static vs Instance Members
Static members belong to the class itself, not to any instance. Instance members belong to each individual object.
dartclass Counter { // Static — shared across ALL instances, belongs to class static int totalCount = 0; static const int maxLimit = 100; // Instance — unique to EACH object String name; int count = 0; Counter(this.name) { totalCount++; // Track how many Counter objects created } // Instance method — can access both instance AND static members void increment() { count++; print('$name count: $count (total instances: $totalCount)'); } // Static method — can ONLY access static members static void printTotal() { print('Total Counter instances: $totalCount'); // print(name); // ❌ Error — cannot access instance member from static // print(count); // ❌ Error — cannot access instance member from static } // Static factory pattern static Counter createDefault() { return Counter('Default'); } } void main() { final a = Counter('A'); final b = Counter('B'); final c = Counter('C'); a.increment(); // A count: 1 (total instances: 3) b.increment(); // B count: 1 (total instances: 3) // Static accessed via CLASS name, not instance Counter.printTotal(); // Total Counter instances: 3 print(Counter.maxLimit); // 100 // a.printTotal(); // ⚠️ Works but bad practice — use Counter.printTotal() }
Static vs Instance Comparison
| Feature | Static | Instance |
|---|---|---|
| Belongs to | Class | Object |
| Accessed via | text | text |
| Memory | One copy (shared) | One copy per object |
| Can access instance members? | No | Yes |
| Can access static members? | Yes | Yes |
| Use case | Constants, counters, factories, utilities | Object-specific data and behavior |
| Dart example | text text | text text |
Common Static Patterns in Flutter
dart// 1. Route names class AppRoutes { static const String home = '/'; static const String profile = '/profile'; static const String settings = '/settings'; } // 2. API constants class ApiConstants { static const String baseUrl = 'https://api.example.com'; static const Duration timeout = Duration(seconds: 30); } // 3. Helper/Utility methods class Validators { static bool isEmail(String value) => value.contains('@'); static bool isPhone(String value) => value.length == 10; } // Usage Navigator.pushNamed(context, AppRoutes.profile); if (Validators.isEmail(input)) { ... }
Real-World Flutter Example — Clean Architecture
dart// DOMAIN LAYER — abstract class (contract) abstract class UserRepository { Future<User> getUser(String id); Future<void> saveUser(User user); } // DATA LAYER — implements (fulfills the contract) class UserRepositoryImpl implements UserRepository { final ApiClient api; final LocalDatabase db; UserRepositoryImpl(this.api, this.db); Future<User> getUser(String id) async { try { return await api.fetchUser(id); } catch (_) { return await db.getCachedUser(id); } } Future<void> saveUser(User user) async { await api.updateUser(user); await db.cacheUser(user); } } // TESTING — implements (mock for unit tests) class MockUserRepository implements UserRepository { Future<User> getUser(String id) async => User(id: id, name: 'Mock User'); Future<void> saveUser(User user) async => print('Mock save'); } // PRESENTATION — uses abstract type, doesn't care about implementation class UserViewModel { final UserRepository repo; // Could be real or mock UserViewModel(this.repo); Future<User> loadUser(String id) => repo.getUser(id); }
Quick Decision Flowchart
textDo you need to REUSE parent code? ├── YES → Does the child "is-a" parent? │ ├── YES → Use extends │ └── NO → Use mixin (with) │ └── NO → Do you need a contract/API guarantee? ├── YES → Use implements └── NO → Use a regular class Multiple sources of code needed? ├── YES → Use mixins (with) — can mix multiple └── NO → Use extends (single parent) Need exhaustive pattern matching? → Use sealed class Need to prevent others from extending? → Use final class Need to force implements-only? → Use interface class
Key Takeaway:
= inherit code + constructor chain.textextends= contract only, no inheritance, no parent constructor.textimplements(mixin) = plug-in reusable abilities from multiple sources. Understanding constructor behavior (textwithcalls parent,textextendsdoesn't) is the #1 interview question in this topic.textimplements