Dart Object-Oriented Programming

Introduction to OOP & Classes in Dart

45 min Lesson 1 of 8

What Is Object-Oriented Programming?

Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects rather than functions and logic. An object is a bundle of related data (called properties or fields) and behavior (called methods). Think of it like the real world: a car has properties (color, speed, fuel level) and behaviors (accelerate, brake, turn). OOP lets you model your code the same way.

Dart is a fully object-oriented language -- in fact, everything in Dart is an object. Even numbers, strings, and booleans are objects with methods. When you write 42.toString() or 'hello'.toUpperCase(), you are calling methods on objects.

Why OOP Matters for Flutter

Flutter is built entirely on OOP concepts. Every widget you create is a class. Every screen is an object. Understanding OOP is not optional for Flutter -- it is the foundation everything is built upon. Here is a quick preview:

Flutter is OOP

// Every Flutter widget is a class
class MyApp extends StatelessWidget {   // Inheritance
  const MyApp({super.key});             // Constructor

  @override                             // Polymorphism
  Widget build(BuildContext context) {  // Method
    return MaterialApp(home: HomeScreen());
  }
}
Note: You don’t need to understand this Flutter code yet. The point is that classes, constructors, inheritance, and methods are everywhere in Flutter. This tutorial will teach you all of these concepts step by step.

The Four Pillars of OOP

OOP is built on four fundamental principles:

  • Encapsulation -- Bundling data and methods together, and controlling access to internal details.
  • Abstraction -- Hiding complex implementation details and showing only what is necessary.
  • Inheritance -- Creating new classes based on existing ones, reusing code.
  • Polymorphism -- Objects of different classes responding to the same method in different ways.

We will explore each of these throughout this tutorial. For now, let’s start with the most important building block: classes.

Creating Your First Class

A class is a blueprint for creating objects. It defines what properties an object has and what it can do. An object (also called an instance) is a concrete thing created from that blueprint.

Defining a Simple Class

// The class is the blueprint
class Person {
  // Properties (data)
  String name = '';
  int age = 0;

  // Method (behavior)
  void introduce() {
    print('Hi, I am $name and I am $age years old.');
  }
}

void main() {
  // Create an object (instance) from the blueprint
  Person person1 = Person();
  person1.name = 'Ahmed';
  person1.age = 25;
  person1.introduce();  // Hi, I am Ahmed and I am 25 years old.

  // Create another object from the same blueprint
  Person person2 = Person();
  person2.name = 'Sara';
  person2.age = 30;
  person2.introduce();  // Hi, I am Sara and I am 30 years old.

  // Each object is independent
  print(person1.name);  // Ahmed (unchanged)
}
Analogy: A class is like a cookie cutter, and objects are the cookies. One cookie cutter (class) can make many cookies (objects), and each cookie can be decorated differently (different property values).

Constructors

Setting properties one by one after creating an object is tedious and error-prone. A constructor is a special method that runs automatically when you create an object, letting you initialize properties right away.

Default Constructor

class Person {
  String name;
  int age;

  // Constructor -- same name as the class
  Person(this.name, this.age);
  // 'this.name' is Dart shorthand for: this.name = name

  void introduce() {
    print('Hi, I am $name and I am $age years old.');
  }
}

void main() {
  // Now we pass values directly when creating the object
  Person person = Person('Ahmed', 25);
  person.introduce();  // Hi, I am Ahmed and I am 25 years old.

  // Much cleaner than setting each property manually!
  Person person2 = Person('Sara', 30);
  person2.introduce();  // Hi, I am Sara and I am 30 years old.
}

Named Parameters in Constructors

For classes with many properties, named parameters make the code more readable. Wrap them in curly braces {}:

Named Parameters

class User {
  String username;
  String email;
  int age;
  bool isActive;

  // Named parameters with required and default values
  User({
    required this.username,
    required this.email,
    required this.age,
    this.isActive = true,  // Default value
  });

  void displayInfo() {
    print('User: $username ($email), Age: $age, Active: $isActive');
  }
}

void main() {
  User user1 = User(
    username: 'ahmed_dev',
    email: 'ahmed@example.com',
    age: 25,
  );
  user1.displayInfo();
  // User: ahmed_dev (ahmed@example.com), Age: 25, Active: true

  User user2 = User(
    username: 'sara_code',
    email: 'sara@example.com',
    age: 30,
    isActive: false,  // Override default
  );
  user2.displayInfo();
  // User: sara_code (sara@example.com), Age: 30, Active: false
}
Note: In Flutter, almost every widget uses named parameters with required. This pattern will become second nature as you build Flutter apps. For example: Text('Hello', style: TextStyle(fontSize: 20)).

Properties and Methods

Properties hold the object’s data. Methods define the object’s behavior. Together, they form a complete object.

Properties and Methods in Action

class BankAccount {
  String owner;
  double _balance;  // Underscore makes it "private"

  BankAccount({required this.owner, double initialBalance = 0})
      : _balance = initialBalance;

  // Getter -- read-only access to private data
  double get balance => _balance;

  // Method: deposit money
  void deposit(double amount) {
    if (amount <= 0) {
      print('Error: Deposit amount must be positive.');
      return;
    }
    _balance += amount;
    print('Deposited \$${amount.toStringAsFixed(2)}. New balance: \$${_balance.toStringAsFixed(2)}');
  }

  // Method: withdraw money
  bool withdraw(double amount) {
    if (amount <= 0) {
      print('Error: Withdrawal amount must be positive.');
      return false;
    }
    if (amount > _balance) {
      print('Error: Insufficient funds.');
      return false;
    }
    _balance -= amount;
    print('Withdrew \$${amount.toStringAsFixed(2)}. New balance: \$${_balance.toStringAsFixed(2)}');
    return true;
  }

  // Method: display account info
  void displayInfo() {
    print('Account Owner: $owner, Balance: \$${_balance.toStringAsFixed(2)}');
  }
}

void main() {
  BankAccount account = BankAccount(owner: 'Ahmed', initialBalance: 1000);
  account.displayInfo();     // Account Owner: Ahmed, Balance: $1000.00
  account.deposit(500);      // Deposited $500.00. New balance: $1500.00
  account.withdraw(200);     // Withdrew $200.00. New balance: $1300.00
  account.withdraw(2000);    // Error: Insufficient funds.
  print(account.balance);    // 1300.0 (using the getter)
  // account._balance = 999; // ERROR in other files! (private)
}
Important: In Dart, the underscore prefix (_) makes a property or method library-private, not class-private. This means it is accessible within the same file but hidden from other files. This is Dart’s approach to encapsulation. We will cover this in more detail in a later lesson.

The this Keyword

The this keyword refers to the current instance of the class. It is useful when parameter names match property names, or when you need to return the current object:

Using this

class Rectangle {
  double width;
  double height;

  // 'this' in constructor shorthand
  Rectangle(this.width, this.height);

  double get area => width * height;
  double get perimeter => 2 * (width + height);

  // Method chaining using 'this'
  Rectangle scale(double factor) {
    width *= factor;
    height *= factor;
    return this;  // Return the same object for chaining
  }

  void display() {
    print('Rectangle: ${width}x$height, Area: $area, Perimeter: $perimeter');
  }
}

void main() {
  Rectangle rect = Rectangle(10, 5);
  rect.display();  // Rectangle: 10.0x5.0, Area: 50.0, Perimeter: 30.0

  // Method chaining
  rect.scale(2).display();  // Rectangle: 20.0x10.0, Area: 200.0, Perimeter: 60.0
}

Multiple Objects from One Class

Each object created from a class is completely independent. Changing one object does not affect another:

Independent Objects

class Counter {
  String label;
  int _count = 0;

  Counter(this.label);

  int get count => _count;

  void increment() => _count++;
  void decrement() => _count--;
  void reset() => _count = 0;

  @override
  String toString() => '$label: $_count';
}

void main() {
  Counter likes = Counter('Likes');
  Counter views = Counter('Views');

  likes.increment();
  likes.increment();
  likes.increment();

  views.increment();
  views.increment();
  views.increment();
  views.increment();
  views.increment();

  print(likes);  // Likes: 3
  print(views);  // Views: 5

  // They are completely independent
  likes.reset();
  print(likes);  // Likes: 0
  print(views);  // Views: 5 (unchanged)
}

The toString Method

Every class in Dart inherits from Object, which has a toString() method. By default, it returns something like Instance of 'ClassName'. Override it to provide a meaningful string representation:

Overriding toString

class Product {
  String name;
  double price;
  int quantity;

  Product({required this.name, required this.price, this.quantity = 0});

  double get totalValue => price * quantity;

  @override
  String toString() {
    return 'Product($name, \$${price.toStringAsFixed(2)}, qty: $quantity)';
  }
}

void main() {
  Product laptop = Product(name: 'Laptop', price: 999.99, quantity: 5);

  // Without toString override: Instance of 'Product'
  // With toString override:
  print(laptop);  // Product(Laptop, $999.99, qty: 5)

  // Also works in string interpolation
  print('Item: $laptop, Total: \$${laptop.totalValue}');
  // Item: Product(Laptop, $999.99, qty: 5), Total: $4999.95
}

Class vs Object: The Key Distinction

Remember:
• A class is a blueprint/template -- it defines the structure.
• An object (or instance) is a concrete thing created from the class -- it holds actual data.
• You can create many objects from one class.
• Each object has its own copy of the properties.
• All objects share the same methods (the code), but each runs methods on its own data.

Practical Example: Building a Task Manager

Task Manager with Classes

class Task {
  String title;
  String description;
  bool isCompleted;
  DateTime createdAt;

  Task({
    required this.title,
    this.description = '',
    this.isCompleted = false,
  }) : createdAt = DateTime.now();

  void complete() {
    isCompleted = true;
    print('Task "$title" marked as completed.');
  }

  void reopen() {
    isCompleted = false;
    print('Task "$title" reopened.');
  }

  @override
  String toString() {
    String status = isCompleted ? '[DONE]' : '[TODO]';
    return '$status $title';
  }
}

class TaskManager {
  String projectName;
  List<Task> _tasks = [];

  TaskManager(this.projectName);

  // Add a new task
  void addTask(String title, {String description = ''}) {
    _tasks.add(Task(title: title, description: description));
    print('Added: "$title"');
  }

  // Get pending tasks
  List<Task> get pendingTasks =>
      _tasks.where((t) => !t.isCompleted).toList();

  // Get completed tasks
  List<Task> get completedTasks =>
      _tasks.where((t) => t.isCompleted).toList();

  // Complete a task by index
  void completeTask(int index) {
    if (index >= 0 && index < _tasks.length) {
      _tasks[index].complete();
    }
  }

  // Display summary
  void showSummary() {
    print('\n--- $projectName ---');
    print('Total: ${_tasks.length} | Pending: ${pendingTasks.length} | Done: ${completedTasks.length}');
    for (var task in _tasks) {
      print('  $task');
    }
  }
}

void main() {
  TaskManager manager = TaskManager('Flutter App');

  manager.addTask('Set up project structure');
  manager.addTask('Create login screen');
  manager.addTask('Add API integration');
  manager.addTask('Write unit tests');

  manager.completeTask(0);
  manager.completeTask(1);

  manager.showSummary();
  // --- Flutter App ---
  // Total: 4 | Pending: 2 | Done: 2
  //   [DONE] Set up project structure
  //   [DONE] Create login screen
  //   [TODO] Add API integration
  //   [TODO] Write unit tests
}

Practice Exercise

Open DartPad and build the following: (1) Create a Student class with properties: name, studentId, and a private list of _grades (List<double>). (2) Add a constructor with required named parameters for name and studentId. (3) Add an addGrade(double grade) method that only accepts grades between 0 and 100. (4) Add a getter averageGrade that calculates and returns the average. (5) Add a getter letterGrade that returns A/B/C/D/F based on the average. (6) Override toString() to show the student’s info. (7) Create 3 Student objects, add grades to each, and print their info.