🎯 Complete Tutorial — Basics to Advanced

Dart Object-Oriented Programming

A comprehensive guide with super-clear explanations, real-world examples, and hands-on exercises.

01

🧱 Introduction to OOP in Dart

Object-Oriented Programming (OOP) is a way of writing code by organizing it around objects — things that have data (properties) and behavior (methods).

Think of it like this:

Imagine you're describing a Car. A car has properties like color, brand, speed, and behaviors like drive(), brake(), honk(). In OOP, we create a blueprint (called a Class) and then build actual cars (called Objects) from that blueprint.

The 4 Pillars of OOP:

🔒

Encapsulation

Hiding internal details and exposing only what's needed

🧬

Inheritance

A class can inherit properties and methods from another class

🎭

Polymorphism

One method, many forms (behaves differently based on the object)

🎨

Abstraction

Showing only essential features, hiding complexity

02

🏗️ Classes & Objects

What is a Class?

A class is a blueprint or template that defines what properties and methods an object will have.

What is an Object?

An object is an instance (a real thing created from the blueprint).

dart — Classes & Objects
// 📌 DEFINING A CLASS
class Dog {
  // Properties (also called fields or attributes)
  String name;
  String breed;
  int age;

  // Constructor — runs when you create an object
  Dog(this.name, this.breed, this.age);

  // Method — a function inside a class
  void bark() {
    print('$name says: Woof! Woof! 🐶');
  }

  void info() {
    print('Name: $name, Breed: $breed, Age: $age years');
  }
}

void main() {
  // 📌 CREATING OBJECTS (Instances of the class)
  Dog dog1 = Dog('Buddy', 'Golden Retriever', 3);
  Dog dog2 = Dog('Max', 'German Shepherd', 5);

  dog1.bark();    // Output: Buddy says: Woof! Woof! 🐶
  dog2.info();    // Output: Name: Max, Breed: German Shepherd, Age: 5 years

  // Accessing properties directly
  print(dog1.name);  // Output: Buddy
}
💡 Key Takeaway A class is the recipe, and an object is the dish you cook using that recipe. You can cook many dishes from one recipe!
03

🔨 Constructors

A constructor is a special method that is called automatically when you create an object. It's used to initialize the object's properties.

3.1 Default Constructor

If you don't define any constructor, Dart provides a default constructor with no arguments.

dart — Default Constructor
class Cat {
  String name = 'Unknown';
  int age = 0;

  // No constructor defined — Dart provides a default one
}

void main() {
  Cat cat = Cat();
  print(cat.name);  // Output: Unknown
}

3.2 Parameterized Constructor

Pass values when creating an object.

dart — Parameterized Constructor
class Student {
  String name;
  int rollNumber;
  double marks;

  // Parameterized Constructor (shorthand syntax)
  Student(this.name, this.rollNumber, this.marks);

  void display() {
    print('Student: $name | Roll: $rollNumber | Marks: $marks');
  }
}

void main() {
  Student s1 = Student('Alice', 101, 95.5);
  Student s2 = Student('Bob', 102, 88.0);

  s1.display();  // Student: Alice | Roll: 101 | Marks: 95.5
  s2.display();  // Student: Bob | Roll: 102 | Marks: 88.0
}

3.3 Named Constructor

Dart allows multiple constructors using named constructors.

dart — Named Constructor
class Point {
  double x;
  double y;

  // Regular constructor
  Point(this.x, this.y);

  // Named constructor — creates a point at the origin
  Point.origin()
      : x = 0,
        y = 0;

  // Named constructor — creates a point on the X-axis
  Point.onXAxis(double x)
      : x = x,
        y = 0;

  // Named constructor — from a Map
  Point.fromMap(Map<String, double> map)
      : x = map['x'] ?? 0,
        y = map['y'] ?? 0;

  void display() {
    print('Point($x, $y)');
  }
}

void main() {
  Point p1 = Point(3, 4);
  Point p2 = Point.origin();
  Point p3 = Point.onXAxis(7);
  Point p4 = Point.fromMap({'x': 10, 'y': 20});

  p1.display();  // Point(3.0, 4.0)
  p2.display();  // Point(0.0, 0.0)
  p3.display();  // Point(7.0, 0.0)
  p4.display();  // Point(10.0, 20.0)
}
💡 When to use Named Constructors? Use them when you want different ways to create an object — like creating a user from JSON, from a database, or with default values.

3.4 Const Constructor

Creates compile-time constant objects. The object is created once and reused (saves memory).

dart — Const Constructor
class Color {
  final int r;
  final int g;
  final int b;

  // Const constructor — all fields must be final
  const Color(this.r, this.g, this.b);
}

void main() {
  const red = Color(255, 0, 0);
  const red2 = Color(255, 0, 0);

  // Both point to the SAME object in memory!
  print(identical(red, red2));  // Output: true
}
💡 When to use Const Constructors? When you have objects that never change — like colors, configurations, or theme data.

3.5 ⭐ Factory Constructor

A factory constructor doesn't always create a new instance. It can return an existing instance, a subclass instance, or even decide which type of object to create.

dart — Factory Constructor (Singleton Pattern)
class Logger {
  static Logger? _instance;  // Store the single instance

  final String name;

  // Private constructor — prevents creating objects from outside
  Logger._internal(this.name);

  // Factory constructor — returns the SAME instance every time (Singleton Pattern)
  factory Logger(String name) {
    _instance ??= Logger._internal(name);  // Create only if null
    return _instance!;
  }

  void log(String message) {
    print('[$name] $message');
  }
}

void main() {
  Logger logger1 = Logger('AppLogger');
  Logger logger2 = Logger('AnotherLogger');

  logger1.log('Starting app...');
  logger2.log('This is logger2');

  // Both are the SAME object!
  print(identical(logger1, logger2));  // Output: true
  print(logger2.name);  // Output: AppLogger (not AnotherLogger!)
}

Another Factory Example — Returning Different Subclasses:

dart — Factory Returning Subclasses
abstract class Shape {
  double getArea();

  // Factory that decides which subclass to create
  factory Shape.create(String type, double value) {
    switch (type) {
      case 'circle':
        return Circle(value);
      case 'square':
        return Square(value);
      default:
        throw ArgumentError('Unknown shape: $type');
    }
  }
}

class Circle extends Shape {
  double radius;
  Circle(this.radius);

  @override
  double getArea() => 3.14159 * radius * radius;
}

class Square extends Shape {
  double side;
  Square(this.side);

  @override
  double getArea() => side * side;
}

void main() {
  Shape circle = Shape.create('circle', 5);
  Shape square = Shape.create('square', 4);

  print('Circle Area: ${circle.getArea()}');  // Circle Area: 78.53975
  print('Square Area: ${square.getArea()}');  // Square Area: 16.0
}
⭐ When to use Factory Constructors? Singleton pattern — only one instance of a class
Caching — return an existing object instead of creating new ones
Returning subtypes — decide which subclass to return based on conditions
Complex initialization — when you need logic before creating the object

3.6 Redirecting Constructor

A constructor that calls another constructor in the same class.

dart — Redirecting Constructor
class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

  // Redirecting constructor — calls the main constructor
  Rectangle.square(double side) : this(side, side);
}

void main() {
  Rectangle rect = Rectangle(10, 5);
  Rectangle square = Rectangle.square(7);

  print('Rectangle: ${rect.width} x ${rect.height}');  // 10.0 x 5.0
  print('Square: ${square.width} x ${square.height}');  // 7.0 x 7.0
}
04

🔒 Encapsulation (Getters & Setters)

Encapsulation means hiding the internal state of an object and controlling access through getters and setters.

In Dart, a property is made private by prefixing it with an underscore (_). Private members are only accessible within the same file (library).

dart — Encapsulation with Getters & Setters
class BankAccount {
  String _accountHolder;   // Private
  double _balance;         // Private

  BankAccount(this._accountHolder, this._balance);

  // GETTER — allows reading the balance
  double get balance => _balance;

  // GETTER — allows reading the account holder name
  String get accountHolder => _accountHolder;

  // SETTER — controls how balance is modified
  set balance(double amount) {
    if (amount < 0) {
      print('❌ Balance cannot be negative!');
    } else {
      _balance = amount;
    }
  }

  // Method to deposit money
  void deposit(double amount) {
    if (amount > 0) {
      _balance += amount;
      print('✅ Deposited ₹$amount. New Balance: ₹$_balance');
    } else {
      print('❌ Deposit amount must be positive!');
    }
  }

  // Method to withdraw money
  void withdraw(double amount) {
    if (amount > _balance) {
      print('❌ Insufficient balance!');
    } else if (amount <= 0) {
      print('❌ Withdrawal amount must be positive!');
    } else {
      _balance -= amount;
      print('✅ Withdrew ₹$amount. Remaining Balance: ₹$_balance');
    }
  }
}

void main() {
  BankAccount account = BankAccount('Jobin', 10000);

  print('Account: ${account.accountHolder}');
  print('Balance: ₹${account.balance}');

  account.deposit(5000);    // ✅ Deposited ₹5000. New Balance: ₹15000
  account.withdraw(3000);   // ✅ Withdrew ₹3000. Remaining Balance: ₹12000
  account.withdraw(50000);  // ❌ Insufficient balance!
  account.balance = -500;   // ❌ Balance cannot be negative!
}
💡 Why Encapsulation? Protects data from accidental modification
Validates data before setting it
Controls what other parts of the code can see and change
05

🧬 Inheritance

Inheritance allows a class to inherit properties and methods from another class. The class being inherited from is called the parent (or superclass), and the one inheriting is called the child (or subclass).

dart — Inheritance Basics
// Parent Class (Superclass)
class Animal {
  String name;
  int age;

  Animal(this.name, this.age);

  void eat() {
    print('$name is eating 🍖');
  }

  void sleep() {
    print('$name is sleeping 💤');
  }
}

// Child Class (Subclass) — inherits from Animal
class Dog extends Animal {
  String breed;

  // Call parent constructor using super
  Dog(String name, int age, this.breed) : super(name, age);

  void bark() {
    print('$name says: Woof! 🐕');
  }
}

// Another Child Class
class Cat extends Animal {
  bool isIndoor;

  Cat(String name, int age, this.isIndoor) : super(name, age);

  void meow() {
    print('$name says: Meow! 🐱');
  }
}

void main() {
  Dog dog = Dog('Buddy', 3, 'Labrador');
  Cat cat = Cat('Whiskers', 2, true);

  dog.eat();    // Buddy is eating 🍖
  dog.sleep();  // Buddy is sleeping 💤
  dog.bark();   // Buddy says: Woof! 🐕

  cat.eat();    // Whiskers is eating 🍖
  cat.meow();   // Whiskers says: Meow! 🐱

  print('${dog.name} is a ${dog.breed}');  // Buddy is a Labrador
}

Method Overriding

A child class can replace a parent's method with its own version.

dart — Method Overriding
class Vehicle {
  void start() {
    print('Vehicle is starting...');
  }
}

class Car extends Vehicle {
  @override
  void start() {
    print('Car engine roars to life! 🚗');
  }
}

class ElectricCar extends Vehicle {
  @override
  void start() {
    print('Electric car silently powers on... ⚡🚗');
  }
}

void main() {
  Vehicle v = Vehicle();
  Car c = Car();
  ElectricCar e = ElectricCar();

  v.start();  // Vehicle is starting...
  c.start();  // Car engine roars to life! 🚗
  e.start();  // Electric car silently powers on... ⚡🚗
}

Using super to Call Parent Methods

dart — Using super
class Employee {
  String name;
  double baseSalary;

  Employee(this.name, this.baseSalary);

  double calculateSalary() {
    return baseSalary;
  }

  void display() {
    print('Employee: $name | Salary: ₹${calculateSalary()}');
  }
}

class Manager extends Employee {
  double bonus;

  Manager(String name, double baseSalary, this.bonus)
      : super(name, baseSalary);

  @override
  double calculateSalary() {
    // Call parent's method and add bonus
    return super.calculateSalary() + bonus;
  }
}

void main() {
  Employee emp = Employee('Alice', 50000);
  Manager mgr = Manager('Bob', 70000, 15000);

  emp.display();  // Employee: Alice | Salary: ₹50000.0
  mgr.display();  // Employee: Bob | Salary: ₹85000.0
}
06

🎭 Polymorphism

Polymorphism means "many forms." The same method call can behave differently depending on the object that calls it.

dart — Polymorphism in Action
class Shape {
  String name;

  Shape(this.name);

  double area() {
    return 0;
  }
}

class Circle extends Shape {
  double radius;
  Circle(this.radius) : super('Circle');

  @override
  double area() => 3.14159 * radius * radius;
}

class Rectangle extends Shape {
  double width, height;
  Rectangle(this.width, this.height) : super('Rectangle');

  @override
  double area() => width * height;
}

class Triangle extends Shape {
  double base, height;
  Triangle(this.base, this.height) : super('Triangle');

  @override
  double area() => 0.5 * base * height;
}

void main() {
  // A list of Shape — but each one behaves differently!
  List<Shape> shapes = [
    Circle(5),
    Rectangle(10, 4),
    Triangle(6, 8),
  ];

  // Polymorphism in action — same method call, different behavior
  for (Shape shape in shapes) {
    print('${shape.name} area: ${shape.area().toStringAsFixed(2)}');
  }
  // Output:
  // Circle area: 78.54
  // Rectangle area: 40.00
  // Triangle area: 24.00
}
💡 Why is this powerful? You can write code that works with the parent type but it automatically handles all child types correctly. Add a new shape? Just create a new subclass — the existing loop works without changes!
07

🎨 Abstraction (Abstract Classes)

An abstract class is a class that cannot be instantiated directly. It serves as a contract — it defines methods that subclasses must implement.

dart — Abstract Classes
// Abstract class — cannot create objects of this directly
abstract class Database {
  // Abstract methods — no body, must be implemented by subclasses
  void connect();
  void disconnect();
  Future<List<Map<String, dynamic>>> query(String sql);

  // Regular method — can have a body (inherited by subclasses)
  void log(String message) {
    print('[DB LOG] $message');
  }
}

class MySQLDatabase extends Database {
  @override
  void connect() {
    log('Connecting to MySQL...');
    print('✅ Connected to MySQL');
  }

  @override
  void disconnect() {
    print('🔌 Disconnected from MySQL');
  }

  @override
  Future<List<Map<String, dynamic>>> query(String sql) async {
    log('Executing: $sql');
    await Future.delayed(Duration(seconds: 1));
    return [{'id': 1, 'name': 'Alice'}];
  }
}

class MongoDatabase extends Database {
  @override
  void connect() {
    log('Connecting to MongoDB...');
    print('✅ Connected to MongoDB');
  }

  @override
  void disconnect() {
    print('🔌 Disconnected from MongoDB');
  }

  @override
  Future<List<Map<String, dynamic>>> query(String sql) async {
    log('Executing MongoDB query...');
    await Future.delayed(Duration(seconds: 1));
    return [{'id': 1, 'title': 'Document 1'}];
  }
}

void main() async {
  // Database db = Database();  // ❌ ERROR! Cannot instantiate abstract class

  Database db = MySQLDatabase();  // ✅ Using a concrete subclass
  db.connect();

  var results = await db.query('SELECT * FROM users');
  print('Results: $results');

  db.disconnect();
}
💡 When to use Abstract Classes? When you want to force subclasses to implement certain methods, while optionally sharing some common code.
08

📋 Interfaces

In Dart, every class is automatically an interface. When you implement a class, you must provide your own implementation of all its methods and properties.

dart — Implementing Multiple Interfaces
// These act as interfaces
class Printable {
  void printData() {}
}

class Exportable {
  void exportToCSV() {}
  void exportToPDF() {}
}

// A class can implement MULTIPLE interfaces
class Report implements Printable, Exportable {
  String title;
  String content;

  Report(this.title, this.content);

  @override
  void printData() {
    print('📄 Printing Report: $title');
    print('Content: $content');
  }

  @override
  void exportToCSV() {
    print('📊 Exporting "$title" to CSV...');
  }

  @override
  void exportToPDF() {
    print('📑 Exporting "$title" to PDF...');
  }
}

void main() {
  Report report = Report('Sales Q4', 'Revenue increased by 25%');
  report.printData();
  report.exportToCSV();
  report.exportToPDF();
}

Difference: extends vs implements

Featureextends (Inheritance)implements (Interface)
Inherits code?✅ Yes❌ No (must rewrite everything)
How many?Only ONE classMULTIPLE classes
PurposeReuse parent's codeFollow a contract
09

🧩 Mixins

Mixins let you share code between multiple classes without using inheritance. Think of them as "plug-in" behaviors.

dart — Mixins
// Mixin — a reusable chunk of behavior
mixin Flyable {
  void fly() {
    print('Flying through the sky! 🦅');
  }
}

mixin Swimmable {
  void swim() {
    print('Swimming in the water! 🏊');
  }
}

mixin Walkable {
  void walk() {
    print('Walking on land! 🚶');
  }
}

// A Duck can walk, swim, AND fly!
class Duck with Walkable, Swimmable, Flyable {
  String name;
  Duck(this.name);

  void quack() {
    print('$name says: Quack! 🦆');
  }
}

// A Fish can only swim
class Fish with Swimmable {
  String name;
  Fish(this.name);
}

// A Penguin can walk and swim, but NOT fly
class Penguin with Walkable, Swimmable {
  String name;
  Penguin(this.name);
}

void main() {
  Duck duck = Duck('Donald');
  duck.quack();   // Donald says: Quack! 🦆
  duck.walk();    // Walking on land! 🚶
  duck.swim();    // Swimming in the water! 🏊
  duck.fly();     // Flying through the sky! 🦅

  Penguin penguin = Penguin('Skipper');
  penguin.walk(); // Walking on land! 🚶
  penguin.swim(); // Swimming in the water! 🏊
  // penguin.fly(); // ❌ ERROR! Penguin doesn't have Flyable
}

Mixin with on keyword (restrict to specific classes)

dart — Mixin with on keyword
class Musician {
  void play() {
    print('Playing music...');
  }
}

// This mixin can ONLY be used on classes that extend Musician
mixin Vocalist on Musician {
  void sing() {
    print('Singing beautifully! 🎤');
    play();  // Can access Musician's methods
  }
}

class Singer extends Musician with Vocalist {}

void main() {
  Singer singer = Singer();
  singer.play();  // Playing music...
  singer.sing();  // Singing beautifully! 🎤  → Playing music...
}
10

📊 Enums

Enums define a fixed set of constant values. Use them when a variable can only be one of a few specific options.

Simple Enum

dart — Simple Enum
enum Status { pending, active, inactive, banned }

void main() {
  Status userStatus = Status.active;

  switch (userStatus) {
    case Status.pending:
      print('Account is pending approval');
      break;
    case Status.active:
      print('Account is active ✅');
      break;
    case Status.inactive:
      print('Account is inactive');
      break;
    case Status.banned:
      print('Account is banned ❌');
      break;
  }
}

Enhanced Enum (Dart 2.17+)

Enums can have properties, constructors, and methods!

dart — Enhanced Enum
enum Planet {
  mercury(diameter: 4879, distanceFromSun: 57.9),
  venus(diameter: 12104, distanceFromSun: 108.2),
  earth(diameter: 12756, distanceFromSun: 149.6),
  mars(diameter: 6792, distanceFromSun: 227.9);

  final double diameter;         // in km
  final double distanceFromSun;  // in million km

  const Planet({required this.diameter, required this.distanceFromSun});

  String get description =>
      '$name: Diameter = ${diameter}km, Distance from Sun = ${distanceFromSun}M km';
}

void main() {
  for (Planet planet in Planet.values) {
    print(planet.description);
  }
}
11

📦 Generics

Generics let you write classes and functions that work with any type while keeping type safety.

dart — Generic Class
// A generic class — T is a placeholder for any type
class Box<T> {
  T item;

  Box(this.item);

  T getItem() => item;

  void display() {
    print('Box contains: $item (${item.runtimeType})');
  }
}

void main() {
  Box<String> nameBox = Box('Flutter');
  Box<int> numberBox = Box(42);
  Box<List<double>> listBox = Box([1.1, 2.2, 3.3]);

  nameBox.display();    // Box contains: Flutter (String)
  numberBox.display();  // Box contains: 42 (int)
  listBox.display();    // Box contains: [1.1, 2.2, 3.3] (List<double>)
}

Generic with Constraints

dart — Generic with Constraints
// T must be a subtype of num (int, double, etc.)
class MathBox<T extends num> {
  T value;

  MathBox(this.value);

  double doubled() => value * 2.0;
  bool isPositive() => value > 0;
}

void main() {
  MathBox<int> intBox = MathBox(10);
  MathBox<double> doubleBox = MathBox(3.14);

  print(intBox.doubled());       // 20.0
  print(doubleBox.isPositive()); // true

  // MathBox<String> strBox = MathBox('hello');  // ❌ ERROR! String is not a num
}

Generic Functions

dart — Generic Functions
T findFirst<T>(List<T> items) {
  return items.first;
}

Map<String, T> wrapInMap<T>(String key, T value) {
  return {key: value};
}

void main() {
  print(findFirst<int>([10, 20, 30]));           // 10
  print(findFirst<String>(['a', 'b', 'c']));     // a
  print(wrapInMap('age', 25));                     // {age: 25}
  print(wrapInMap('name', 'Dart'));                // {name: Dart}
}
12

🔧 Static Members

Static members belong to the class itself, not to any specific object. You access them using the class name.

dart — Static Members
class AppConfig {
  static String appName = 'MyApp';
  static String version = '2.1.0';
  static int _requestCount = 0;

  static void trackRequest() {
    _requestCount++;
    print('Total API requests: $_requestCount');
  }

  static String getInfo() {
    return '$appName v$version';
  }
}

class MathUtils {
  static const double pi = 3.14159265359;

  static double circleArea(double radius) => pi * radius * radius;
  static double celsiusToFahrenheit(double c) => (c * 9 / 5) + 32;
}

void main() {
  print(AppConfig.getInfo());                  // MyApp v2.1.0
  AppConfig.trackRequest();                     // Total API requests: 1
  AppConfig.trackRequest();                     // Total API requests: 2

  print(MathUtils.circleArea(5));               // 78.539...
  print(MathUtils.celsiusToFahrenheit(37));     // 98.6
}
13

✨ Extension Methods

Extension methods let you add new functionality to existing classes without modifying them.

dart — Extension Methods
// Add methods to the built-in String class
extension StringExtensions on String {
  String capitalize() {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1).toLowerCase()}';
  }

  bool get isEmail {
    return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);
  }

  String get reversed => split('').reversed.join('');
}

// Add methods to int
extension IntExtensions on int {
  bool get isEven => this % 2 == 0;
  Duration get seconds => Duration(seconds: this);
  Duration get minutes => Duration(minutes: this);
  String get ordinal {
    if (this % 100 >= 11 && this % 100 <= 13) return '${this}th';
    switch (this % 10) {
      case 1: return '${this}st';
      case 2: return '${this}nd';
      case 3: return '${this}rd';
      default: return '${this}th';
    }
  }
}

// Add methods to List
extension ListExtensions<T> on List<T> {
  T? get secondOrNull => length >= 2 ? this[1] : null;
}

void main() {
  print('hello world'.capitalize());   // Hello world
  print('test@email.com'.isEmail);     // true
  print('Dart'.reversed);              // traD

  print(1.ordinal);   // 1st
  print(2.ordinal);   // 2nd
  print(13.ordinal);  // 13th

  print([10, 20, 30].secondOrNull);  // 20
  print([10].secondOrNull);          // null
}
14

⚠️ Exception Handling

Exceptions are errors that happen during runtime (while your program is running). Dart gives you tools to catch these errors and handle them gracefully.

14.1 Try, Catch, Finally

dart — Try, Catch, Finally
void main() {
  // BASIC TRY-CATCH
  try {
    int result = 10 ~/ 0;  // Integer division by zero
    print(result);
  } catch (e) {
    print('Error caught: $e');
  }
  // Output: Error caught: IntegerDivisionByZeroException

  // CATCHING SPECIFIC EXCEPTION TYPES
  try {
    List<int> numbers = [1, 2, 3];
    print(numbers[10]);  // Index out of range
  } on RangeError catch (e) {
    print('Range Error: $e');
  } on FormatException catch (e) {
    print('Format Error: $e');
  } catch (e) {
    print('Unknown Error: $e');  // Catches everything else
  }

  // USING FINALLY — always runs, even if there's an error
  try {
    var result = double.parse('abc');  // Will fail
    print(result);
  } catch (e) {
    print('Cannot parse: $e');
  } finally {
    print('This ALWAYS runs, error or not!');
  }
}

14.2 Catching Error Details (Stack Trace)

dart — Stack Trace
void main() {
  try {
    riskyFunction();
  } catch (e, stackTrace) {
    print('Error: $e');
    print('Stack Trace:\n$stackTrace');
  }
}

void riskyFunction() {
  throw Exception('Something went wrong in riskyFunction!');
}

14.3 Custom Exceptions & Throwing

dart — Custom Exceptions
class InsufficientFundsException implements Exception {
  final double requested;
  final double available;

  InsufficientFundsException(this.requested, this.available);

  @override
  String toString() =>
      'InsufficientFundsException: Requested ₹$requested but only ₹$available available';
}

class InvalidAgeException implements Exception {
  final String message;
  InvalidAgeException(this.message);

  @override
  String toString() => 'InvalidAgeException: $message';
}

class Wallet {
  double _balance;

  Wallet(this._balance);

  double get balance => _balance;

  void withdraw(double amount) {
    if (amount <= 0) {
      throw ArgumentError('Amount must be positive');
    }
    if (amount > _balance) {
      throw InsufficientFundsException(amount, _balance);
    }
    _balance -= amount;
    print('Withdrew ₹$amount. Remaining: ₹$_balance');
  }
}

void validateAge(int age) {
  if (age < 0) {
    throw InvalidAgeException('Age cannot be negative: $age');
  }
  if (age > 150) {
    throw InvalidAgeException('Age is unrealistically high: $age');
  }
  print('Age $age is valid ✅');
}

void main() {
  Wallet wallet = Wallet(1000);

  try {
    wallet.withdraw(500);   // ✅ Withdrew ₹500. Remaining: ₹500
    wallet.withdraw(800);   // ❌ Will throw
  } on InsufficientFundsException catch (e) {
    print(e);
  } on ArgumentError catch (e) {
    print('Invalid argument: $e');
  }

  try {
    validateAge(25);    // Age 25 is valid ✅
    validateAge(-5);    // ❌ Will throw
  } on InvalidAgeException catch (e) {
    print(e);
  }
}

14.4 Rethrow

Sometimes you want to catch an exception, do something (like logging), and then pass it along.

dart — Rethrow
void processData(String data) {
  try {
    var number = int.parse(data);
    print('Parsed: $number');
  } catch (e) {
    print('Logging error: $e');  // Log it
    rethrow;  // Pass it to the caller
  }
}

void main() {
  try {
    processData('abc');
  } catch (e) {
    print('Main caught: $e');
  }
}
// Output:
// Logging error: FormatException: abc
// Main caught: FormatException: abc
15

⏳ Futures & Async/Await

What is Asynchronous Programming?

Imagine you're at a restaurant. You order food and then wait for it to be prepared. In synchronous code, you'd stand at the counter doing nothing until your food is ready. In asynchronous code, you go sit down, check your phone, and the waiter brings your food when it's ready.

15.1 What is a Future?

A Future represents a value that will be available sometime in the future. It can be in one of three states: Uncompleted (still processing), Completed with a value (success!), or Completed with an error (something went wrong).

dart — Basic Future with .then()
Future<String> fetchUserName() {
  // Simulating a network request that takes 2 seconds
  return Future.delayed(Duration(seconds: 2), () {
    return 'Jobin';
  });
}

void main() {
  print('1. Fetching user...');

  fetchUserName().then((name) {
    print('2. Got user: $name');
  }).catchError((error) {
    print('Error: $error');
  });

  print('3. This runs IMMEDIATELY (doesn\'t wait!)');
}

// Output:
// 1. Fetching user...
// 3. This runs IMMEDIATELY (doesn't wait!)
// (after 2 seconds)
// 2. Got user: Jobin

15.2 Async / Await — The Cleaner Way

async and await make asynchronous code look like synchronous code — much easier to read!

dart — Async / Await
Future<String> fetchUser() async {
  await Future.delayed(Duration(seconds: 1));
  return 'Jobin';
}

Future<int> fetchAge(String user) async {
  await Future.delayed(Duration(seconds: 1));
  return 28;
}

Future<String> fetchRole(String user) async {
  await Future.delayed(Duration(seconds: 1));
  return 'Developer';
}

void main() async {
  print('Loading profile...');

  try {
    String user = await fetchUser();
    int age = await fetchAge(user);
    String role = await fetchRole(user);

    print('Name: $user, Age: $age, Role: $role');
  } catch (e) {
    print('Error: $e');
  }

  print('Done!');
}
// Output: Loading profile...
// (after ~3 seconds) Name: Jobin, Age: 28, Role: Developer
// Done!

15.3 Running Futures in Parallel with Future.wait

If your futures are independent, run them simultaneously instead of one after another!

dart — Future.wait (Parallel Execution)
Future<String> fetchUser() async {
  await Future.delayed(Duration(seconds: 2));
  return 'Jobin';
}

Future<List<String>> fetchPosts() async {
  await Future.delayed(Duration(seconds: 3));
  return ['Post 1', 'Post 2', 'Post 3'];
}

Future<int> fetchFollowerCount() async {
  await Future.delayed(Duration(seconds: 1));
  return 1500;
}

void main() async {
  print('Loading dashboard...');
  Stopwatch sw = Stopwatch()..start();

  // ✅ All three run at the SAME TIME
  var results = await Future.wait([
    fetchUser(),
    fetchPosts(),
    fetchFollowerCount(),
  ]);

  String user = results[0] as String;
  List<String> posts = results[1] as List<String>;
  int followers = results[2] as int;

  sw.stop();
  print('User: $user');
  print('Posts: $posts');
  print('Followers: $followers');
  print('Loaded in ${sw.elapsed.inSeconds} seconds');  // ~3 seconds, not 6!
}

15.4 Handling Future Errors

dart — Future Error Handling
Future<int> fetchData(bool shouldFail) async {
  await Future.delayed(Duration(seconds: 1));
  if (shouldFail) {
    throw Exception('Server Error 500!');
  }
  return 42;
}

void main() async {
  // Method 1: try-catch (recommended with async/await)
  try {
    int data = await fetchData(true);
    print('Data: $data');
  } catch (e) {
    print('Caught: $e');  // Caught: Exception: Server Error 500!
  }

  // Method 2: .then() and .catchError()
  fetchData(false)
      .then((value) => print('Success: $value'))
      .catchError((e) => print('Error: $e'));
}

15.5 Useful Future Methods

dart — Future.value, Future.error, Future.any, Future.forEach
void main() async {
  // Future.value — instantly resolves
  int instant = await Future.value(100);
  print('Instant: $instant');  // 100

  // Future.error — instantly fails
  try {
    await Future.error('Oops!');
  } catch (e) {
    print('Error: $e');  // Oops!
  }

  // Future.any — returns the FIRST future to complete
  var fastest = await Future.any([
    Future.delayed(Duration(seconds: 3), () => 'Slow'),
    Future.delayed(Duration(seconds: 1), () => 'Fast'),
    Future.delayed(Duration(seconds: 2), () => 'Medium'),
  ]);
  print('Winner: $fastest');  // Fast

  // Future.forEach — run async operations sequentially
  await Future.forEach<int>([1, 2, 3], (number) async {
    await Future.delayed(Duration(milliseconds: 500));
    print('Processed: $number');
  });
}
16

🌊 Streams

What is a Stream?

A Future gives you one value in the future. A Stream gives you a sequence of values over time — like a conveyor belt.

Real-world examples of Streams: Live chat messages arriving one by one, stock prices updating every second, a file being downloaded chunk by chunk, user typing in a search bar (keystrokes).

16.1 Creating and Listening to Streams

dart — Basic Stream with async* and yield
Stream<int> countStream(int max) async* {
  for (int i = 1; i <= max; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;  // 'yield' sends a value through the stream
  }
}

void main() async {
  print('Stream starting...');

  await for (int number in countStream(5)) {
    print('Received: $number');
  }

  print('Stream complete!');
}
// Output (one per second):
// Received: 1, 2, 3, 4, 5
// Stream complete!

16.2 Stream using StreamController

dart — StreamController
import 'dart:async';

void main() async {
  StreamController<String> controller = StreamController<String>();

  controller.stream.listen(
    (data) {
      print('📩 Received: $data');
    },
    onError: (error) {
      print('❌ Error: $error');
    },
    onDone: () {
      print('✅ Stream closed');
    },
  );

  controller.add('Hello');
  controller.add('World');
  controller.add('From Dart Streams!');
  controller.addError('Something went wrong!');
  controller.add('Still working...');

  await Future.delayed(Duration(seconds: 1));
  await controller.close();
}

16.3 Broadcast Stream (Multiple Listeners)

dart — Broadcast Stream
import 'dart:async';

void main() {
  StreamController<String> controller = StreamController<String>.broadcast();

  // Listener 1
  controller.stream.listen((data) {
    print('Listener 1: $data');
  });

  // Listener 2
  controller.stream.listen((data) {
    print('Listener 2: $data');
  });

  controller.add('Breaking News!');
  controller.add('Flutter 4.0 Released!');
  controller.close();
}

16.4 Stream Transformations

dart — Stream map, where, take, skip
Stream<int> numberStream() async* {
  for (int i = 1; i <= 10; i++) {
    yield i;
  }
}

void main() async {
  // MAP — transform each value
  await for (int n in numberStream().map((n) => n * 2)) {
    print(n);  // 2, 4, 6, 8, 10, 12, 14, 16, 18, 20
  }

  // WHERE — filter values
  await for (int n in numberStream().where((n) => n.isEven)) {
    print(n);  // 2, 4, 6, 8, 10
  }

  // TAKE — only take first N values
  await for (int n in numberStream().take(3)) {
    print(n);  // 1, 2, 3
  }

  // Combine transformations
  await for (int n in numberStream()
      .where((n) => n.isEven)      // Keep even: 2, 4, 6, 8, 10
      .take(3)                      // Take first 3: 2, 4, 6
      .map((n) => n * n)) {         // Square them: 4, 16, 36
    print(n);
  }
}

16.5 Stream to Future Conversions

dart — Stream to Future
void main() async {
  Stream<int> numbers = Stream.fromIterable([1, 2, 3, 4, 5]);

  List<int> list = await numbers.toList();
  print('List: $list');  // [1, 2, 3, 4, 5]

  int first = await Stream.fromIterable([10, 20, 30]).first;
  print('First: $first');  // 10

  int last = await Stream.fromIterable([10, 20, 30]).last;
  print('Last: $last');    // 30

  int sum = await Stream.fromIterable([1, 2, 3, 4, 5])
      .reduce((a, b) => a + b);
  print('Sum: $sum');  // 15
}

16.6 Real-World Stream Example: Live Chat Simulation

dart — Live Chat with Streams
import 'dart:async';

class ChatRoom {
  final _controller = StreamController<Map<String, String>>.broadcast();

  Stream<Map<String, String>> get messages => _controller.stream;

  void sendMessage(String user, String text) {
    _controller.add({
      'user': user,
      'text': text,
      'time': DateTime.now().toString().substring(11, 19),
    });
  }

  void dispose() {
    _controller.close();
  }
}

void main() async {
  ChatRoom room = ChatRoom();

  room.messages.listen((msg) {
    print('[${msg['time']}] ${msg['user']}: ${msg['text']}');
  });

  room.sendMessage('Alice', 'Hey everyone!');
  await Future.delayed(Duration(milliseconds: 500));

  room.sendMessage('Bob', 'Hi Alice! 👋');
  await Future.delayed(Duration(milliseconds: 500));

  room.sendMessage('Alice', 'How is the project going?');
  await Future.delayed(Duration(milliseconds: 500));

  room.sendMessage('Bob', 'Almost done! Just fixing some bugs 🐛');
  room.dispose();
}

16.7 StreamSubscription — Pause, Resume, Cancel

dart — StreamSubscription Control
import 'dart:async';

void main() async {
  Stream<int> stream = Stream.periodic(
    Duration(seconds: 1),
    (count) => count + 1,
  ).take(10);

  StreamSubscription<int> subscription = stream.listen((data) {
    print('Data: $data');
  });

  await Future.delayed(Duration(seconds: 3));
  print('⏸️ Pausing...');
  subscription.pause();

  await Future.delayed(Duration(seconds: 2));
  print('▶️ Resuming...');
  subscription.resume();

  await Future.delayed(Duration(seconds: 3));
  print('🛑 Cancelling!');
  await subscription.cancel();
}
17

🏋️ Exercises & Challenges

Exercise 1

Class Basics

Create a Book class with properties: title, author, price, pages. Add methods:

  • isLongBook() → returns true if pages > 300
  • applyDiscount(double percent) → reduces the price
  • display() → prints all book info

Create 3 book objects and test all methods.

Exercise 2

Encapsulation

Create a PasswordManager class that:

  • Stores a private _password
  • Has a setter that only accepts passwords with 8+ characters, at least 1 number and 1 uppercase letter
  • Has a method checkPassword(String input) that returns true/false
  • Has a getter that returns a masked version (e.g., "*****ing")
Exercise 3

Inheritance & Polymorphism

Create a PaymentMethod base class with a method processPayment(double amount). Create subclasses:

  • CreditCard — charges 2% fee
  • UPI — no fee
  • BankTransfer — charges flat ₹10 fee

Store all in a List<PaymentMethod> and process ₹1000 through each.

Exercise 4

Abstract Class + Interface

Create an abstract class Notification with method send(String message). Create:

  • EmailNotification — prints "📧 Email: ..."
  • SMSNotification — prints "📱 SMS: ..."
  • PushNotification — prints "🔔 Push: ..."

Create a NotificationService class that accepts a List<Notification> and sends a message through all of them.

Exercise 5

Factory Constructor

Create a Cache<T> class using a factory constructor that:

  • Stores cached items in a static Map
  • Returns existing instance if the key exists
  • Creates new instance if the key doesn't exist
  • Has a getData() method
Exercise 6

Exception Handling

Create a UserRegistration class that throws custom exceptions:

  • InvalidEmailException if email doesn't contain '@'
  • WeakPasswordException if password < 8 chars
  • UserExistsException if username is already taken

Write a register() method and handle all exceptions in main().

Exercise 7

Async/Await

Create a WeatherService class that simulates:

  • fetchLocation() — takes 1 sec, returns "Kerala"
  • fetchWeather(String location) — takes 2 secs, returns "28°C, Partly Cloudy"
  • fetchForecast(String location) — takes 1.5 secs, returns a list of 3 days

Call them using async/await. Then optimize using Future.wait for independent calls.

Exercise 8

Streams

Create a StockTicker class that:

  • Uses a StreamController<Map<String, double>>
  • Emits random stock prices every second for companies: "AAPL", "GOOG", "TSLA"
  • Has a method to get only stocks above a certain price (using where)
  • Has a method to format the output (using map)
  • Supports multiple listeners (broadcast stream)
Exercise 9

Complete Mini-Project — Library Management System

Build a Library Management System using everything you've learned:

  • Book class with properties and methods
  • Member class that extends a Person abstract class
  • Library class that manages books and members
  • Use custom exceptions for: book not found, already borrowed, member limit reached
  • Use a Stream to simulate a live feed of book checkouts
  • Use async/await to simulate database operations
  • Use mixins for Searchable and Sortable behaviors
  • Use enums for BookCategory and MemberType
  • Use generics for a Repository<T> class
Exercise 10

Stream Transformation Chain

Create a stream of integers from 1 to 100. Apply the following chain:

  • Filter only numbers divisible by 3
  • Map them to their square
  • Skip the first 5 results
  • Take the next 10 results
  • Reduce to find the sum

Print each intermediate step.

📝 Quick Reference Cheat Sheet

CLASSclass MyClass { } → var obj = MyClass();
CONSTRUCTORMyClass(this.x, this.y);
NAMEDMyClass.fromMap(Map m) : x = m['x'];
FACTORYfactory MyClass() => _instance;
CONSTconst MyClass(this.x); // fields must be final
PRIVATE_privateField → get field => _privateField;
EXTENDSclass Child extends Parent { }
OVERRIDE@override void method() { }
ABSTRACTabstract class A { void method(); }
IMPLEMENTSclass B implements A { }
MIXINmixin M { } → class C with M { }
ENUMenum Color { red, green, blue }
GENERICSclass Box<T> { T item; }
STATICstatic int count = 0; | ClassName.count
EXTENSIONextension on String { ... }
TRY-CATCHtry { } on TypeError catch(e) { } finally { }
THROWthrow Exception('msg');
CUSTOM ERRclass MyError implements Exception { }
FUTUREFuture<String> fn() async { return 'data'; }
AWAITString data = await fn();
FUTURE.WAITawait Future.wait([f1(), f2()]);
STREAMStream<int> fn() async* { yield 1; }
LISTENstream.listen((data) { });
AWAIT FORawait for (var item in stream) { }
CONTROLLERStreamController<T>() | .broadcast()
TRANSFORMstream.map() | .where() | .take()

🎉 What's Next?

Now that you've mastered Dart OOP, you're ready to level up!

📱

Flutter Apps

Build mobile apps using OOP

🧠

State Management

BLoC, Riverpod, Provider

🏛️

Design Patterns

Singleton, Observer, Repository

🌐

REST API

Integrate with Futures & Streams

Real-time Features

Chat, live data with Streams

Built with 💙 for Dart & Flutter Developers — Happy Coding! 🚀