Module 02: Dart OOP — Classes, Mixins, and Extensions
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:
- Create a
Songclass withtitle,artist,durationInSeconds(allfinal), a constructor usingthis.shorthand, a getterdurationFormattedthat returns a string like"3:45", and atoString()override - Create a
Playlistclass with anameand aList<Song>, methods toaddSong()andremoveSong(), a gettertotalDurationthat sums all songs, and atoString()that lists all songs - 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
- Create three mixins:
Serializable(with atoJson()method),Printable(with aprettyPrint()method), andValidatable(with avalidate()method that returnsbool) - Create a
UserProfileclass that uses all three mixins - Demonstrate that each mixin's methods work on a
UserProfileinstance
Exercise 3: extensions.dart
- Write an extension on
List<int>that addssum,average, andmaxgetters - Write an extension on
DateTimethat adds atimeAgogetter returning a human-readable string like "2 hours ago" or "yesterday" - Write an extension on
Stringthat adds atoTitleCasegetter ("hello world"becomes"Hello World") - 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.