Dart Object-Oriented Programming
A comprehensive guide with super-clear explanations, real-world examples, and hands-on exercises.
🧱 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:
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
🏗️ 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).
// 📌 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
}
🔨 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.
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.
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.
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)
}
3.4 Const Constructor
Creates compile-time constant objects. The object is created once and reused (saves memory).
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
}
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.
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:
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
}
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.
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
}
🔒 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).
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!
}
Validates data before setting it
Controls what other parts of the code can see and change
🧬 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).
// 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.
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
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
}
🎭 Polymorphism
Polymorphism means "many forms." The same method call can behave differently depending on the object that calls it.
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
}
🎨 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.
// 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();
}
📋 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.
// 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
| Feature | extends (Inheritance) | implements (Interface) |
|---|---|---|
| Inherits code? | ✅ Yes | ❌ No (must rewrite everything) |
| How many? | Only ONE class | MULTIPLE classes |
| Purpose | Reuse parent's code | Follow a contract |
🧩 Mixins
Mixins let you share code between multiple classes without using inheritance. Think of them as "plug-in" behaviors.
// 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)
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...
}
📊 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
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!
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);
}
}
📦 Generics
Generics let you write classes and functions that work with any type while keeping type safety.
// 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
// 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
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}
}
🔧 Static Members
Static members belong to the class itself, not to any specific object. You access them using the class name.
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
}
✨ Extension Methods
Extension methods let you add new functionality to existing classes without modifying them.
// 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
}
⚠️ 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
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)
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
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.
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
⏳ 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).
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!
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!
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
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
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');
});
}
🌊 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
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
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)
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
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
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
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
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();
}
🏋️ Exercises & Challenges
Class Basics
Create a Book class with properties: title, author, price, pages. Add methods:
isLongBook()→ returnstrueif pages > 300applyDiscount(double percent)→ reduces the pricedisplay()→ prints all book info
Create 3 book objects and test all methods.
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")
Inheritance & Polymorphism
Create a PaymentMethod base class with a method processPayment(double amount). Create subclasses:
CreditCard— charges 2% feeUPI— no feeBankTransfer— charges flat ₹10 fee
Store all in a List<PaymentMethod> and process ₹1000 through each.
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.
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
Exception Handling
Create a UserRegistration class that throws custom exceptions:
InvalidEmailExceptionif email doesn't contain '@'WeakPasswordExceptionif password < 8 charsUserExistsExceptionif username is already taken
Write a register() method and handle all exceptions in main().
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.
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)
Complete Mini-Project — Library Management System
Build a Library Management System using everything you've learned:
Bookclass with properties and methodsMemberclass that extends aPersonabstract classLibraryclass that manages books and members- Use custom exceptions for: book not found, already borrowed, member limit reached
- Use a
Streamto simulate a live feed of book checkouts - Use
async/awaitto simulate database operations - Use mixins for
SearchableandSortablebehaviors - Use enums for
BookCategoryandMemberType - Use generics for a
Repository<T>class
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.