Introducing Classes and Objects
So far in this tutorial, you have worked with simple values: numbers, strings, lists. That works fine for small programs. But real programs juggle many pieces of information that belong together. A music player, for example, tracks a song's title, artist, duration, and play count — and it needs to do things like play and pause.
A class is a blueprint for bundling related data and behavior together. An object is one concrete thing built from that blueprint. This chapter explains what that means, why it matters, and how to build your first classes in Dart.
Why Bundle Data and Behavior?
Imagine tracking songs with loose variables:
String title1 = 'No Surprises';
String artist1 = 'Radiohead';
int plays1 = 42;Two songs means numbered variables. A hundred songs means chaos. Worse, nothing stops you from mixing things up — setting plays1 to a string, or forgetting which title goes with which artist.
A class solves this by defining one idea — "a song has a title, an artist, and a play count, and you can record a play" — once. Then you create as many song objects as you like, and each one keeps its own data safely inside.
Your First Class
Here is a complete, runnable program:
class Song {
String title;
String artist;
int plays = 0;
Song(this.title, this.artist);
void play() {
plays++;
print('Now playing "$title" by $artist (play #$plays)');
}
}
void main() {
final first = Song('No Surprises', 'Radiohead');
final second = Song('Bags', 'Clairo');
first.play();
first.play();
second.play();
}Run it and you will see:
Now playing "No Surprises" by Radiohead (play #1)
Now playing "No Surprises" by Radiohead (play #2)
Now playing "Bags" by Clairo (play #1)Let's take this apart piece by piece.
Fields: The Data
Lines like String title; inside the class declare fields (also called properties). Each field holds one piece of data. Every object created from the class gets its own copy — first and second each have their own title and plays.
The initializer int plays = 0; gives a starting value, so every new song begins with zero plays.
Constructors: Building an Object
This line is a constructor:
Song(this.title, this.artist);A constructor is a special function that runs when you create an object. Song('No Surprises', 'Radiohead') calls it. The shorthand this.title means "the field named title on this object." Dart lets you write Song(this.title, this.artist) as a compact way of saying: take two parameters and store them in the fields with the same names. Without the shorthand, you would write Song(String title, String artist) { this.title = title; this.artist = artist; } — same result, more typing.
Methods: The Behavior
play() is a method: a function that belongs to the class. Inside a method, plays++ refers to the field on the current object — whichever song you called it on. first.play() bumps first's counter; second.play() bumps second's.
Objects and References
One detail trips up many learners: a variable holds a reference to an object, not the object itself. That means two variables can point at the same object.
void main() {
final a = Song('Bags', 'Clairo');
final b = a; // b and a refer to the SAME song
b.play(); // changes the object both variables share
print(a.plays); // prints 1, not 0
}If you wanted b to be a separate copy, you would construct a new Song with the same values instead of assigning.
Private Fields and Control
By default, anyone using your class can change any field:
final song = Song('Bags', 'Clairo');
song.plays = -500; // nothing stops thisSometimes that is fine. Other times you want rules — a play count should never go negative, for instance. Dart's rule for privacy is simple: names starting with an underscore (_) are private to the file (technically, to the library) they are declared in. Here is a version that protects the play count:
class Counter {
int _count = 0; // private field
int get count => _count; // read-only view
void increment() => _count++;
void reset() => _count = 0;
}
void main() {
final c = Counter();
c.increment();
c.increment();
print(c.count); // 2
// c._count = 99; // not allowed outside this file
}The get count line is a getter: it looks like a field from the outside (c.count, no parentheses) but runs code underneath. This gives you a dial you can turn in one place later — for example, logging every read — without changing the code that uses the class.
Multiple Constructors and Named Parameters
Classes often need more than one way to be built. Dart supports named parameters, written in curly braces, which make constructor calls self-explanatory:
class Song {
final String title;
final String artist;
final int plays;
Song({required this.title, required this.artist, this.plays = 0});
@override
String toString() => '"$title" by $artist ($plays plays)';
}
void main() {
final s = Song(title: 'Dreams', artist: 'Fleetwood Mac');
print(s);
}Two new things appear here:
finalmarks fields that never change after construction. If a song's title cannot change, say so — the compiler will then enforce it.requiredmeans the caller must supply the value;this.plays = 0gives a default for anything not marked required.
The @override String toString() line customizes how the object prints. toString is inherited from every Dart object's shared ancestor; overriding it (and marking the override with @override) is the idiomatic way to make print(s) show something useful.
How It Fits Together
Show diagram source
classDiagram
class Song {
+String title
+String artist
+int plays
+play()
}
Song <|-- first : object
Song <|-- second : object
class first {
title = "No Surprises"
plays = 2
}
class second {
title = "Bags"
plays = 1
}The class exists once; each object carries its own data and shares the methods.
Where You Will Take This Next
You now know how to define a class, construct objects, protect data with privacy and getters, and print objects nicely. The next chapter goes deeper into constructors and members — including initializer lists and factory constructors — and after that you will learn how classes relate to each other through inheritance. Objects are the backbone of almost everything in Dart, including Flutter, so the time you spend here pays off everywhere.
Exercise
You write final b = a; where a is a Song object, then call b.play(). Does a.plays change, and why?
Show answer
Yes. b and a refer to the same object, so any method called through either variable affects that one shared object.
Exercise
Why does a field named _count behave differently from one named count?
Show answer
The leading underscore makes the field private to the library (in practice, the file) where it is declared, so outside code cannot read or write it directly. A public getter can still expose a read-only view.
Exercise
Write a Timer class with a private seconds field (starting at 0), a public getter, and an add(int s) method that only accepts positive values.
Show answer
class Timer { int seconds = 0; int get seconds => seconds; void add(int s) { if (s > 0) _seconds += s; } }
Spotted a mistake, or code that does not work?