Module 02: Dart OOP — Classes, Mixins, and Extensions

Dart & Widget Basics

Object-oriented Dart — building blocks for Flutter widgets

🎯

Teach: How to structure code with classes, constructors, inheritance, mixins, and extensions in Dart. See: How these patterns map directly to Flutter widget architecture. Feel: Comfortable reading and writing class-based Dart code — the foundation of every Flutter app.


Why OOP Matters for Flutter

🎯

Teach: Why understanding object-oriented programming is non-negotiable for Flutter development. See: The direct connection between Dart classes and Flutter widget code. Feel: Motivated to learn OOP thoroughly because every Flutter widget depends on it.

Every Flutter widget is a class. Every screen, every button, every animation controller — it's all classes. If you don't understand Dart's object-oriented features, Flutter code will look like hieroglyphics.

The good news? Dart's OOP is clean and predictable. No surprises, no weird edge cases. Let's build it up piece by piece.

🎙️

You might be thinking, "I've seen classes before — do I really need a whole module on this?" Yes. Because Dart has a few tricks that other languages don't — named constructors, factory constructors, mixins, extensions, and the cascade operator. These aren't obscure features. Flutter uses all of them in its core APIs.


Class Anatomy

🎯

Teach: How a Dart class is structured — fields, constructors, methods, getters, and setters. See: A complete class with the this. shorthand, computed getters, and toString(). Feel: Comfortable reading and writing Dart classes from scratch.

A class in Dart groups related data (fields) and behavior (methods) together:

class Dog {
  // Fields
  String name;
  String breed;
  int age;

  // Constructor
  Dog(this.name, this.breed, this.age);

  // Method
  String bark() => '$name says: Woof!';

  // Getter
  bool get isOld => age > 10;

  // Setter
  set nickname(String value) => name = value;

  // toString for nice printing
  @override
  String toString() => 'Dog($name, $breed, age $age)';
}

void main() {
  var rex = Dog('Rex', 'German Shepherd', 5);
  print(rex.bark());    // Rex says: Woof!
  print(rex.isOld);     // false
  print(rex);           // Dog(Rex, German Shepherd, age 5)
}
🎙️

Take a second to appreciate how compact this is. In Java or C#, you'd need twenty lines of private fields, getters, setters, and a constructor that assigns each field manually. Dart gives you the this.name shorthand in the constructor, arrow-function methods, computed getters with the get keyword, and operator-style setters. The language was designed to let you describe data and behavior without ceremony. Every one of these features shows up constantly in Flutter widget code, so learn to read this anatomy fluently.

Let's break down the key pieces:

Fields and the this Shorthand

Instead of writing a verbose constructor that manually assigns each field:

// The long way
Dog(String name, String breed, int age) {
  this.name = name;
  this.breed = breed;
  this.age = age;
}

Dart lets you use the this.parameter shorthand:

// The Dart way
Dog(this.name, this.breed, this.age);

Same result, one line. You'll see this pattern in virtually every Flutter widget.

Getters and Setters

Getters (get) let you compute a value from existing fields. Setters (set) let you control how a field is assigned:

class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

  // Computed property — no stored field needed
  double get area => width * height;
  double get perimeter => 2 * (width + height);

  // Setter with validation
  set dimensions(List<double> values) {
    width = values[0];
    height = values[1];
  }
}
💡

In Dart, getters look like fields from the outside but compute their values on the fly. Use them for derived values instead of storing redundant data.


Constructors: Three Flavors

🎯

Teach: The three types of constructors in Dart and when to use each. See: Real patterns you'll encounter in Flutter's own source code. Feel: Ready to read any Flutter widget constructor without confusion.

1. Standard Constructor

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);
}

var p = Point(3.0, 4.0);

2. Named Constructors

A class can have multiple constructors with different names:

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

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

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

var a = Point(3.0, 4.0);
var b = Point.origin();
var c = Point.fromMap({'x': 1.0, 'y': 2.0});
🎙️

Named constructors are everywhere in Flutter. EdgeInsets.all(16), EdgeInsets.symmetric(horizontal: 8), BorderRadius.circular(12) — those are all named constructors. They give you multiple ways to create the same type of object, each optimized for a different use case.

3. Factory Constructors

A factory constructor can return an existing instance, a subtype, or run logic before creating the object:

class Logger {
  static final Map<String, Logger> _cache = {};
  final String name;

  // Private constructor
  Logger._internal(this.name);

  // Factory — returns cached instance if it exists
  factory Logger(String name) {
    return _cache.putIfAbsent(name, () => Logger._internal(name));
  }

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

void main() {
  var a = Logger('API');
  var b = Logger('API');
  print(identical(a, b));  // true — same instance!
}

Factory constructors are the Dart way to implement patterns like singletons and caching. In Flutter, you'll see them in theming and service layers.


Inheritance with extends

🎯

Teach: How inheritance works in Dart using extends, @override, and super. See: A parent-child class hierarchy with polymorphism in action. Feel: Ready to understand extends StatelessWidget and other Flutter patterns.

Inheritance lets a class inherit fields and methods from a parent class:

class Animal {
  final String name;

  Animal(this.name);

  String speak() => '$name makes a sound';
}

class Cat extends Animal {
  final bool isIndoor;

  Cat(super.name, {this.isIndoor = true});

  @override
  String speak() => '$name says: Meow!';
}

class Dog extends Animal {
  Dog(super.name);

  @override
  String speak() => '$name says: Woof!';
}

void main() {
  Animal cat = Cat('Whiskers');
  Animal dog = Dog('Rex');

  print(cat.speak());  // Whiskers says: Meow!
  print(dog.speak());  // Rex says: Woof!

  // Polymorphism in action:
  var animals = <Animal>[cat, dog];
  for (var animal in animals) {
    print(animal.speak());
  }
}

Key points: - extends creates an "is-a" relationship (a Cat is an Animal) - @override marks methods you're replacing from the parent - super.name passes the argument up to the parent constructor - A variable typed as Animal can hold a Cat or Dog (polymorphism)

🎙️

In Flutter, StatelessWidget and StatefulWidget are classes you extend. When you write class MyApp extends StatelessWidget, you're saying "MyApp is a StatelessWidget — give me all the widget powers, and I'll implement the build() method myself."


Abstract Classes

🎯

Teach: How abstract classes define contracts that subclasses must fulfill. See: An abstract Shape class with concrete Circle and Square implementations. Feel: The elegance of programming to an interface rather than a specific implementation.

Abstract classes define a contract — they declare methods that subclasses must implement, but don't provide implementations themselves:

abstract class Shape {
  // Abstract method — no body, just the signature
  double area();
  double perimeter();

  // Concrete method — has an implementation
  String describe() => 'Area: ${area()}, Perimeter: ${perimeter()}';
}

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

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

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

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

  @override
  double area() => side * side;

  @override
  double perimeter() => 4 * side;
}

void main() {
  // Shape s = Shape();  // ERROR! Can't instantiate abstract class
  Shape circle = Circle(5);
  print(circle.describe());  // Area: 78.53975, Perimeter: 31.4159
}

Abstract classes are blueprints. They say "any Shape must be able to calculate its area and perimeter" without dictating how.

🎙️

Flutter's core classes StatelessWidget and StatefulWidget are both abstract. They define that every widget must implement build() or createState(), but they let you decide what those methods actually do. When you write class MyButton extends StatelessWidget and then implement build(), you're fulfilling a contract the Flutter team wrote years ago. That contract is what lets Flutter's rendering engine call build() on any widget and trust that something will come back. Abstract classes are how you enforce consistency in a large codebase without tying your hands.


Mixins: Multiple Inheritance, Done Right

🎯

Teach: What mixins are, how they differ from inheritance, and when to use them. See: How mixins let you compose behaviors without the "diamond problem." Feel: The power of mixing in capabilities like LEGO snap-ons.

Most languages forbid multiple inheritance because it creates ambiguity (the "diamond problem"). Dart sidesteps this with mixins — reusable bundles of behavior you can snap onto any class.

mixin Swimming {
  void swim() => print('$runtimeType is swimming');
}

mixin Flying {
  void fly() => print('$runtimeType is flying');
}

mixin Running {
  void run() => print('$runtimeType is running');
}

class Duck with Swimming, Flying, Running {
  String name;
  Duck(this.name);
}

class Penguin with Swimming, Running {
  String name;
  Penguin(this.name);
}

void main() {
  var donald = Duck('Donald');
  donald.swim();  // Duck is swimming
  donald.fly();   // Duck is flying
  donald.run();   // Duck is running

  var tux = Penguin('Tux');
  tux.swim();     // Penguin is swimming
  tux.run();      // Penguin is running
  // tux.fly();   // ERROR! Penguins can't fly — no Flying mixin
}

The with keyword attaches mixins. Each mixin adds its methods to the class.

When to Use Mixins vs. Inheritance

  • Inheritance (extends): "A Cat is an Animal" — shared identity
  • Mixins (with): "A Duck can swim, fly, and run" — shared capabilities

In Flutter, the most common mixin you'll encounter is TickerProviderStateMixin, which gives animation capabilities to a State class:

class _MyWidgetState extends State<MyWidget> with TickerProviderStateMixin {
  // Now this state class can provide Tickers for animations
}
💡

Use extends for "is-a" relationships. Use with (mixins) for "can-do" capabilities. A class can only extend one parent, but it can use as many mixins as it needs.

🎙️

Mixins solve a problem most languages dodge. In Java, if you want two capabilities from different parents, you're stuck with interfaces and a lot of copy-pasted implementation. Dart lets you write the behavior once in a mixin and bolt it onto any class that needs it. You'll use this pattern constantly in Flutter for animation. The mixin SingleTickerProviderStateMixin gives your State class everything it needs to drive an AnimationController, without forcing you to inherit from some "AnimatedState" base class. Mix and match, don't lock into one family tree.


Extensions: Adding Powers to Existing Types

🎯

Teach: How to add new methods to existing types you did not write using Dart extensions. See: Custom methods added to String and int that read like built-in features. Feel: Impressed by how extensions keep code fluent and discoverable.

What if you want to add a method to String or int — types you didn't write? Extensions let you do exactly that:

extension StringExtras on String {
  String get reversed => split('').reversed.join('');
  bool get isPalindrome => this == reversed;
  String truncate(int maxLength) =>
      length <= maxLength ? this : '${substring(0, maxLength)}...';
}

extension IntExtras on int {
  bool get isEven => this % 2 == 0;
  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';
    }
  }
}

void main() {
  print('racecar'.isPalindrome);   // true
  print('hello'.reversed);         // olleh
  print('Long sentence here'.truncate(10));  // Long sente...
  print(1.ordinal);   // 1st
  print(42.ordinal);  // 42nd
}
🎙️

Extensions are Dart's answer to "I wish this type had a method for that." Instead of writing a utility function like reverseString(s), you can write s.reversed. It reads better, discovers better in autocomplete, and keeps your code fluent.


The Cascade Operator (..)

🎯

Teach: How the .. cascade operator chains multiple calls on the same object. See: A before-and-after comparison showing repetitive code becoming clean and fluent. Feel: Appreciation for a small syntax feature that makes a big difference in readability.

The cascade operator lets you make multiple calls on the same object without repeating its name:

class EmailBuilder {
  String to = '';
  String subject = '';
  String body = '';

  void send() => print('Sending to $to: $subject');
}

void main() {
  // Without cascades — repetitive
  var email = EmailBuilder();
  email.to = 'campbell@example.com';
  email.subject = 'Welcome!';
  email.body = 'Hello and welcome to the team.';
  email.send();

  // With cascades — clean and fluent
  EmailBuilder()
    ..to = 'campbell@example.com'
    ..subject = 'Welcome!'
    ..body = 'Hello and welcome to the team.'
    ..send();
}

The .. returns the object itself instead of the method's return value, allowing you to chain calls. You'll see this in Flutter for configuring objects that don't use named constructors.

🎙️

The cascade operator looks weird at first — those double dots tripped me up the first time I saw them. But give it a week and you'll miss it when you switch back to other languages. The key mental model is: regular dot . returns whatever the method returns, but cascade .. returns the object you called the method on. That tiny difference unlocks fluent builder patterns without any extra plumbing in your class. You'll see it pop up in Path drawing APIs, in Paint configuration, and anywhere Flutter uses a builder-style object.


There Are No Dumb Questions

🎯

Teach: Answers to common questions about classes, abstract classes, mixins, and factory constructors. See: Clear distinctions between concepts that beginners often confuse. Feel: Confident you understand when to use each OOP feature.

Q: Why can't I just use top-level functions instead of classes? A: You can — and sometimes you should. But classes bundle related data and behavior together. When you have a User with a name, email, and a method to sendVerification(), a class is the natural container. Flutter requires classes for widgets because it needs the build() method pattern.

Q: What's the difference between abstract class and mixin? A: An abstract class defines a type (you can use it as a type annotation like Shape myShape). A mixin defines reusable behavior but isn't a type you'd typically use in annotations. Abstract classes can have constructors; mixins cannot. Use abstract classes for "what something is," mixins for "what something can do."

Q: When would I use a factory constructor instead of a named constructor? A: Factory constructors are for when you need logic before creating the object — caching (return an existing instance), returning a subtype, or parsing/validation. Named constructors are just alternate ways to initialize the same class. If new always creates a fresh instance, use a named constructor. If you might not, use factory.

Q: Can a mixin have state (fields)? A: Yes! Mixins can declare fields and methods. Any class that uses the mixin gets those fields. Just be aware that each class gets its own copy of the fields — they're not shared.

🎙️

The abstract-class-versus-mixin question trips up even experienced developers. Here's the quickest way to decide: if you want to describe what a thing IS — "a Cat is an Animal" — reach for an abstract class and extend it. If you want to describe what a thing CAN DO — "a Duck can swim, fly, and run" — reach for a mixin and use with. Same with factory versus named constructors. Factory is for when you need logic before deciding what to return — like caching or returning a subclass. Named is just a friendlier label for a plain constructor.


Sharpen Your Pencil

🎯

Teach: How to apply classes, mixins, and extensions through hands-on coding exercises. See: Your own class hierarchies, mixin compositions, and extension methods running in Dart. Feel: The OOP concepts solidifying as you build real structures with them.

🔄

Where this fits: Every Flutter widget is a class with constructors, fields, and methods. Understanding OOP in Dart is not optional — it's the skeleton of every Flutter app you'll ever build.

Exercise 1: classes.dart

Build a small class hierarchy for a music library:

  1. Create a Song class with title, artist, durationInSeconds (all final), a constructor using this. shorthand, a getter durationFormatted that returns a string like "3:45", and a toString() override
  2. Create a Playlist class with a name and a List<Song>, methods to addSong() and removeSong(), a getter totalDuration that sums all songs, and a toString() that lists all songs
  3. Create at least 3 songs and a playlist, then print the playlist
class Song {
  final String title;
  final String artist;
  final int durationInSeconds;

  Song(this.title, this.artist, this.durationInSeconds);

  String get durationFormatted {
    var minutes = durationInSeconds ~/ 60;
    var seconds = durationInSeconds % 60;
    return '$minutes:${seconds.toString().padLeft(2, '0')}';
  }

  @override
  String toString() => '$title by $artist ($durationFormatted)';
}

Exercise 2: mixins.dart

  1. Create three mixins: Serializable (with a toJson() method), Printable (with a prettyPrint() method), and Validatable (with a validate() method that returns bool)
  2. Create a UserProfile class that uses all three mixins
  3. Demonstrate that each mixin's methods work on a UserProfile instance

Exercise 3: extensions.dart

  1. Write an extension on List<int> that adds sum, average, and max getters
  2. Write an extension on DateTime that adds a timeAgo getter returning a human-readable string like "2 hours ago" or "yesterday"
  3. Write an extension on String that adds a toTitleCase getter ("hello world" becomes "Hello World")
  4. Test all your extensions in main()
🎙️

Here's a challenge for the extensions exercise: after you write them, look up if Dart or any popular package already provides these. You'll often find that extension methods you wish existed... already do in packages like collection or intl. Knowing how to write them helps you understand how those packages work.


📝 Module Quiz

Test what you learned. 45 seconds per question. Passing score is set per module. Your best attempt is saved in your browser so you can track progress -- nothing is sent to a server.

What's Next?

🎯

Teach: What the next module covers and why async programming is critical for Flutter. See: A preview of Futures, Streams, and the event loop. Feel: Ready to tackle the final Dart pillar before returning to Flutter widgets.

You now have classes, constructors, inheritance, mixins, extensions, and cascades in your toolkit. That's the OOP foundation you need for Flutter. In Module 03, we tackle asynchronous programming — Futures, async/await, and Streams. This is what lets your app fetch data from the internet, read files, and handle events without freezing the UI.

🎙️

Two modules down on Dart, one to go. Async is the last piece of the puzzle before we finally touch widgets. And honestly, async is the concept that separates "I wrote some Dart" from "I can build real Flutter apps." Every HTTP call, every database read, every file load has to be async in Flutter — otherwise the UI freezes. So take the next module seriously. Once you're fluent in Futures and Streams, we pivot hard into Flutter widgets and the course really starts to click.

1 / 1