Constructors, Fields, and Methods in Depth
This chapter assumes you have already met classes and objects in Dart — the previous chapter introduced a class as a blueprint and an object as a thing built from that blueprint. Now we go deeper into the three parts every class is made of: fields (the data an object holds), constructors (the special code that builds a new object), and methods (the actions an object can perform).
Fields: Where an Object Keeps Its Data
A field is a variable that belongs to a class. Every object created from the class gets its own copy.
class Book {
String title = 'Untitled';
int pages = 0;
}Here every Book starts with the title 'Untitled' and zero pages. You can also mark a field final, which means it can be set once and never changed:
class Book {
final String title; // must be set during construction
Book(this.title);
}A field that starts with an underscore, like _secret, is private: it can only be used inside the same file. Dart has no private keyword — the underscore is the convention the whole language and its tools follow.
Constructors: How Objects Are Born
A constructor is a special method that runs when you create a new object with the new-style syntax Book('Dune'). In Dart you can write it in several ways.
1. The shorthand constructor
The most common form uses this.fieldName in the parameter list to assign a parameter straight to a field:
class Book {
final String title;
final int pages;
Book(this.title, this.pages);
}this.title means "the title field of the object being built." This one line replaces what other languages need five lines for.
2. Named constructors
A class can have several constructors with different names, each useful for a different situation:
class Book {
final String title;
final int pages;
Book(this.title, this.pages);
Book.unknown() : title = 'Unknown', pages = 0;
Book.sample() : title = 'Sample Text', pages = 12;
}
void main() {
final a = Book('Dune', 412);
final b = Book.unknown();
print(b.title); // Unknown
}Book.unknown() and Book.sample() are called named constructors. The part after the colon (: title = 'Unknown', ...) is an initializer list — code that runs before the constructor body and can only set fields, not read them yet. This is the only place you can initialize final fields with logic rather than parameters.
3. Redirecting constructors
One constructor can hand off the work to another:
class Book {
final String title;
final int pages;
Book(this.title, this.pages);
Book.paperback(String title) : this(title, 300);
}Methods: What an Object Can Do
A method is a function attached to a class. Inside a method, this refers to the object the method was called on — though in Dart you rarely need to write this explicitly.
class Book {
final String title;
final int pages;
Book(this.title, this.pages);
String describe() => '$title runs $pages pages.';
bool isShort() => pages < 200;
}
void main() {
final b = Book('Hobbit', 310);
print(b.describe()); // Hobbit runs 310 pages.
print(b.isShort()); // false
}The arrow syntax => expression is shorthand for a method whose whole body is one return expression.
Getters and setters
A getter looks like a field from the outside but is computed on demand:
class Book {
final String title;
final int pages;
Book(this.title, this.pages);
double get pagesPerDay => pages / 30; // computed, no stored field
}Callers write b.pagesPerDay with no parentheses, just like a field. A setter (rare in idiomatic Dart) lets assignment trigger code:
class Temperature {
double _celsius = 0;
set fahrenheit(double f) => _celsius = (f - 32) / 1.8;
double get fahrenheit => _celsius * 1.8 + 32;
}Overriding toString
Every Dart object inherits a toString() method. Overriding it makes printing useful:
class Book {
final String title;
Book(this.title);
@override
String toString() => 'Book($title)';
}
void main() {
print(Book('Emma')); // Book(Emma) instead of Instance of 'Book'
}The @override annotation is optional but valuable: it makes the compiler check that a method with that name actually exists in the parent class, catching typos.
Putting It Together
caption: The life of a Dart object, from construction to cleanup. alt: A flow chart with seven steps: a constructor is called, the initializer list runs, field initializers and the constructor body finish, the object becomes ready, its methods and getters are used, the object becomes unreachable, and the garbage collector reclaims its memory.
Two things to note in that diagram. First, the initializer list really does run before anything else — that is why final fields must be set there or in the shorthand parameters. Second, Dart manages memory for you: once no variable points to an object anymore, the garbage collector (the runtime's automatic memory cleaner) frees it. You never write destructor code.
A Warning About Instance Methods and State
Common Errors at This Level
- "The final variable must be initialized." You declared a
finalfield but no constructor sets it. Fix: add it to the constructor's parameters or initializer list. - Calling an instance method without an object.
describe()alone fails; you needb.describe(). - Using
thisin the initializer list. The object does not fully exist yet, so you cannot call its methods there — only set fields. - Forgetting
@overrideand misspelling a parent method name. Without the annotation, the compiler silently treats your typo as a brand-new method.
Exercise
You write Book(this.title) but forgot to declare a title field in the class. What happens?
Show answer
The compiler reports an error: the shorthand this.title requires an actual field named title to assign to. Add final String title; (or String title;) to the class.
Exercise
Which two places can initialize a final field of a class?
Show answer
In the constructor's parameter list via shorthand (Book(this.title)) or in the initializer list (Book.unknown() : title = 'Unknown';). A final field cannot be assigned anywhere else.
Exercise
What is the difference between a getter and a regular method in Dart, from the caller's point of view?
Show answer
A getter is used without parentheses, like a field (b.pagesPerDay), while a method is called with parentheses (b.describe()). Internally both can run code.
Spotted a mistake, or code that does not work?