Dart: Functions & Classes
With the basics of variables and types out of the way, this lesson covers how Dart organizes
behavior (functions) and behavior + data together (classes).
Since every widget in Flutter is itself a class, and every widget's build() method is
itself a function, this is genuinely foundational material for everything that follows.
Defining a Function
A Dart function declares a return type, a name, a parameter list in parentheses, and a body in curly braces.
int add(int a, int b) {
return a + b;
}
void greet(String name) {
print('Hello, $name!');
}
// calling them
int sum = add(2, 3); // 5
greet('Alice'); // Hello, Alice!
void is used as the return type when a function doesn't return a value — you'll see
it constantly on methods that perform an action, like setState callbacks.
Arrow Syntax for Single-Expression Functions
When a function's entire body is just one expression that gets returned, Dart lets you skip the
curly braces and return keyword entirely using the arrow syntax
(=>). This is extremely common in Flutter code, especially inside widget classes.
// Full form
int add(int a, int b) {
return a + b;
}
// Equivalent arrow form -- identical behavior, less code
int add(int a, int b) => a + b;
bool isEven(int n) => n % 2 == 0;
Named Parameters
Dart supports named parameters — parameters that are passed by name at the call site rather than by position, wrapped in curly braces in the function signature. This is used constantly in Flutter, since widget constructors rely heavily on it for readability.
void greet({required String name, String greeting = 'Hello'}) {
print('$greeting, $name!');
}
// called using name: value syntax
greet(name: 'Alice'); // Hello, Alice!
greet(name: 'Bob', greeting: 'Hey'); // Hey, Bob!
Two things are happening in that signature: required means name
must be provided every time — the compiler enforces it, so it's a compile error to
omit it. greeting, on the other hand, has a default value of
'Hello', so it's optional — leave it out and Dart uses the default.
Text('Hi', style: TextStyle(fontSize: 20)) — this is exactly the mechanism you just learned.Optional Positional Parameters
As a brief contrast, Dart also supports optional positional parameters using square brackets instead of curly braces. These are passed by position (not by name) and are simply omitted if not needed.
String fullName(String first, [String? middle, String? last]) {
if (middle != null && last != null) return '$first $middle $last';
if (last != null) return '$first $last';
return first;
}
fullName('Alice'); // Alice
fullName('Alice', 'Marie', 'Smith'); // Alice Marie Smith
In practice, named parameters are far more common in Flutter/Dart code because they make call sites self-documenting — you rarely see square-bracket optional parameters outside a handful of core library APIs.
Defining a Class
Classes bundle data (fields) and behavior (methods) together. Dart has a convenient shorthand
constructor syntax — this.fieldName in the constructor's parameter list — that
assigns an incoming parameter directly to a field of the same name, without writing a body at all.
class Person {
String name;
int age;
// shorthand constructor: this.name / this.age assign the
// incoming arguments straight to the fields -- no body needed
Person(this.name, this.age);
void introduce() {
print('Hi, I\'m $name and I\'m $age years old.');
}
}
Person alice = Person('Alice', 28);
alice.introduce(); // Hi, I'm Alice and I'm 28 years old.
You can also use named parameters directly in a constructor, which is exactly the pattern every widget class uses:
class Product {
String title;
double price;
Product({required this.title, required this.price});
}
Product item = Product(title: 'Mug', price: 9.99);
Named Constructors
A class can have more than one constructor by giving alternate constructors a name after a dot. This is handy for expressing different, meaningful ways to create an instance.
class Person {
String name;
int age;
Person(this.name, this.age);
// A named constructor -- Person.guest()
Person.guest() : name = 'Guest', age = 0;
}
Person visitor = Person.guest();
print(visitor.name); // Guest
Inheritance with extends and super
A class can inherit fields and methods from another class using extends. The
subclass's constructor can forward arguments to the parent class's constructor using
super — this is exactly what every custom widget does when it extends
StatelessWidget or StatefulWidget.
class Animal {
String name;
Animal(this.name);
void speak() {
print('$name makes a sound.');
}
}
class Dog extends Animal {
Dog(String name) : super(name); // forwards name to Animal's constructor
@override
void speak() {
print('$name barks.');
}
}
Dog rex = Dog('Rex');
rex.speak(); // Rex barks.
The @override annotation above documents (and lets the
compiler verify) that speak() is intentionally replacing the parent class's version.
Mixins with the with Keyword
Dart's mixins are a genuinely distinctive feature: a way to share a chunk of
reusable code across classes that don't share a common parent, without traditional multiple
inheritance. You define a mixin with the mixin keyword, and any class can pull it in
with with.
mixin Flyer {
void fly() => print('Flying!');
}
class Bird extends Animal with Flyer {
Bird(String name) : super(name);
}
Bird sparrow = Bird('Sparrow');
sparrow.fly(); // Flying!
Bird gets fly() "for free" from the Flyer mixin, on top of
whatever it inherits from Animal via extends. You'll encounter mixins
again later in this track (for example, SingleTickerProviderStateMixin when working
with animations) — for now, just recognize the pattern when you see with in a class
declaration.