Functions in Dart
This is Part 7 of the Dart Fundamentals series. Functions are where Dart's design choices really pay off — especially named parameters, which is why Flutter code reads the way it does (Padding(padding: ..., child: ...)). Let's master every flavour.
The basics: a typed function
A Dart function declares its return type, name, and typed parameters:
int add(int a, int b) {
return a + b;
}
print(add(2, 3)); // 5
The parameters here (a, b) are positional and required — you must pass them, in order. That's the default. Everything else in this post is a variation on how parameters are passed.
Arrow syntax => — one-expression functions
When a function just returns a single expression, the { return ...; } is noise. The fat arrow => replaces it:
int add(int a, int b) => a + b;
bool isEven(int n) => n % 2 == 0;
String greet(String name) => 'Hello, $name!';
=> expr is exactly shorthand for { return expr; }. The catch: it only works with a single expression, not statements. You can't put an if block or a loop after => — for those you need the full { } body.
Named parameters — call-site clarity
Positional arguments have a readability problem. What does this even mean?
createUser('Asha', 30, true, false); // ...what are true and false?
Named parameters fix it. Wrap parameters in { } and callers pass them by name, in any order:
void createUser({String? name, int? age, bool isAdmin = false}) {
print('$name, $age, admin: $isAdmin');
}
createUser(name: 'Asha', age: 30, isAdmin: true);
createUser(age: 25, name: 'Ben'); // order doesn't matter
Now the call site documents itself. This is the reason Flutter widgets are readable.
required — named but mandatory
By default a named parameter is optional (and so must be nullable or have a default). When a named parameter is actually mandatory, mark it required:
void createUser({required String name, int age = 0}) {
print('$name is $age');
}
createUser(name: 'Asha'); // ✅ age defaults to 0
// createUser(age: 30); // ❌ compile error — name is required
So you get the best of both: named clarity and a compile-time guarantee that the important ones are supplied.
Default values
Any optional parameter (named or positional) can have a default, used when the caller omits it:
void connect({String host = 'localhost', int port = 8080}) {
print('$host:$port');
}
connect(); // localhost:8080
connect(port: 3000); // localhost:3000
Optional positional parameters [ ]
There's a second way to make parameters optional: wrap them in square brackets. These stay positional (order matters) but can be left off the end:
String greet(String name, [String? title, String greeting = 'Hi']) {
final who = title != null ? '$title $name' : name;
return '$greeting, $who!';
}
greet('Asha'); // Hi, Asha!
greet('Asha', 'Dr.'); // Hi, Dr. Asha!
greet('Asha', 'Dr.', 'Hello'); // Hello, Dr. Asha!
Optional positionals ([ ]) without a default are nullable and default to null.
{ } vs [ ] — which to use?
| Style | Syntax | Passed by | Best when… |
| -------------- | ------ | --------- | ----------------------------------------------------- |
| Named | { } | name | Several params, or booleans/clarity matters (default) |
| Optional pos. | [ ] | position | One or two obvious trailing extras |
Rule of thumb: prefer named parameters for anything with more than ~2 arguments, or whenever a
true/false/number at the call site would be cryptic. Reach for[ ]only for short, obvious optional tails.
You can mix required positional with one optional group (but not both [ ] and { } in the same function):
void log(String message, {String level = 'info'}) { /* ... */ }
log('Server started'); // info
log('Disk full', level: 'warning'); // warning
Functions are values (first-class)
In Dart, a function is just another object you can store, pass, and return. The type of a function describes its signature:
int square(int x) => x * x;
// store a function in a variable
int Function(int) op = square;
print(op(5)); // 25
// pass a function as an argument
void apply(int value, int Function(int) fn) => print(fn(value));
apply(4, square); // 16
int Function(int) reads as "a function taking an int and returning an int."
Anonymous functions (lambdas)
You don't have to name a function to use it. An anonymous function is written inline, and pairs perfectly with the collection methods from Part 6:
var nums = [1, 2, 3, 4];
nums.map((n) => n * 2); // arrow-style anonymous fn
nums.where((n) => n.isEven);
nums.forEach((n) { // block-body anonymous fn
print('Item: $n');
});
(n) => n * 2 is a nameless function passed straight in. This is everyday Dart.
Tear-offs — passing a named function directly
When an anonymous function would just forward its argument to another function, skip the wrapper. A tear-off passes the named function itself:
var nums = [1, 2, 3];
nums.forEach((n) => print(n)); // works, but redundant
nums.forEach(print); // tear-off — cleaner, same result
print is a void Function(Object?), so it slots right in. Reach for tear-offs when the lambda body is just (x) => someFunction(x).
Closures — functions that remember
A closure is a function that captures variables from the scope where it was created — and keeps them alive even after that scope is gone. This sounds abstract; an example makes it obvious:
Function makeCounter() {
int count = 0; // local to makeCounter
return () {
count++; // the returned fn "closes over" count
return count;
};
}
var counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
print(counter()); // 3 — count persists between calls
The inner function remembers count even though makeCounter has long since returned. Each call to makeCounter() gets its own fresh count — closures are how you create private, persistent state without a class.
Returning multiple values with records
Need to return two things? Historically you'd make a class or abuse a list. Dart 3 added records — lightweight, anonymous bundles of values — which make this trivial:
// returns a record of (String, int)
(String, int) parseEntry(String raw) {
final parts = raw.split(':');
return (parts[0], int.parse(parts[1]));
}
var (name, age) = parseEntry('Asha:30'); // destructure on the way out
print(name); // Asha
print(age); // 30
You can name the fields for clarity:
({String name, int age}) getUser() => (name: 'Ben', age: 25);
var user = getUser();
print(user.name); // Ben
print(user.age); // 25
Records are perfect for the "I just need to hand back two related values" case without the ceremony of a whole class. (We'll see that var (name, age) = ... destructuring again in Part 9, where patterns really shine.)
Practice Challenges
Attempt before revealing.
Challenge 1 — Arrow-ify. Rewrite this with =>:
double half(double x) {
return x / 2;
}
Show solution
double half(double x) => x / 2;
Single-expression body → fat arrow. Same behaviour, less noise.
Challenge 2 — Make it readable. This call is cryptic: sendEmail('hi@x.com', 'Hello', true, false). Redesign sendEmail so the call documents itself.
Show solution
void sendEmail({
required String to,
required String subject,
bool html = false,
bool urgent = false,
}) { /* ... */ }
sendEmail(to: 'hi@x.com', subject: 'Hello', html: true);
Named parameters turn mystery booleans into labelled, self-documenting arguments — and required keeps the essential ones mandatory.
Challenge 3 — Default + optional. Write power(base, [exponent]) where exponent defaults to 2 (so power(5) → 25, power(2, 3) → 8).
Show solution
int power(int base, [int exponent = 2]) {
var result = 1;
for (var i = 0; i < exponent; i++) {
result *= base;
}
return result;
}
power(5); // 25
power(2, 3); // 8
An optional positional with a default value covers both call shapes.
Challenge 4 — Closure counter with a step. Write makeStepper(int step) that returns a function which adds step each call, starting from 0.
Show solution
int Function() makeStepper(int step) {
int total = 0;
return () {
total += step;
return total;
};
}
var by5 = makeStepper(5);
print(by5()); // 5
print(by5()); // 10
Both step and total are captured by the closure; each stepper keeps its own private running total.
Challenge 5 — Tear-off. Simplify names.map((n) => n.toUpperCase()).toList() using a tear-off, if possible — and if not, explain why.
Show solution
You can't tear off here directly, because toUpperCase is an instance method called on each element, not a standalone function taking the element as its argument. Tear-offs work when the lambda is exactly (x) => f(x). So the lambda stays:
names.map((n) => n.toUpperCase()).toList();
A tear-off would work for something like names.forEach(print), where print(n) takes the element as its sole argument.
Challenge 6 — Return two values. Write minMax(List<int>) that returns both the smallest and largest value using a record, then destructure the result.
Show solution
(int min, int max) minMax(List<int> xs) {
var lo = xs.first, hi = xs.first;
for (final x in xs) {
if (x < lo) lo = x;
if (x > hi) hi = x;
}
return (lo, hi);
}
var (low, high) = minMax([4, 1, 9, 3]);
print('$low..$high'); // 1..9
A record bundles both results; destructuring unpacks them in one line.
Check Yourself (Q&A)
Q1. What's the difference between { } and [ ] parameters?
{ } makes parameters named (passed by name, any order); [ ] makes them optional positional (passed by position, can be omitted from the end). You can't use both groups in one function.
Q2. Are named parameters optional by default?
Yes. A named parameter is optional unless marked required. Optional params must be nullable or have a default value.
Q3. When can I use =>?
Only when the body is a single expression. Statements (loops, if blocks, multiple lines) need the full { } body with return.
Q4. What's a closure? A function that captures and remembers variables from its surrounding scope, keeping them alive after that scope exits — useful for private, persistent state.
Q5. What's a tear-off?
Passing a named function directly where a function value is expected (forEach(print)) instead of wrapping it in a redundant lambda (forEach((x) => print(x))).
Q6. How do I return multiple values?
Use a record: (String, int) f() => ('a', 1);, optionally with named fields, and destructure with var (a, b) = f();.
Wrapping Up
- Default parameters are positional and required; vary that with
{ }(named) or[ ](optional positional). - Mark mandatory named params
required; give optionals default values. =>is shorthand for a single-expression body.- Functions are first-class — store them, pass them, return them; write anonymous functions inline and use tear-offs when a lambda would just forward.
- Closures capture surrounding state; records let you return several values cleanly.
Next, in Part 8, we go beyond interpolation into real string manipulation — splitting, replacing, padding, regex, and the efficient StringBuffer.