Functions, closures and typedefs
Use named and optional parameters, tear-offs and closures correctly, and give function types names that document their contract.
Parameters and defaults
// positional required, then optional positional, then named
String format(String value, [String prefix = '>', String suffix = '<']) =>
'$prefix$value$suffix';
// named parameters: required ones must be marked
String greet({required String name, String greeting = 'Hello', int? times}) {
final count = times ?? 1;
return List.generate(count, (_) => '$greeting, $name!').join(' ');
}
// a named parameter can also be positional-optional; you cannot mix the two styles
void log(String message, {String level = 'INFO', Object? error}) {
print('[$level] $message${error == null ? '' : ' ($error)'}');
}
void main() {
print(format('x')); // >x<
print(format('x', '[')); // [x<
print(greet(name: 'ada')); // Hello, ada!
print(greet(name: 'ada', times: 2));
log('started', level: 'DEBUG');
log('failed', error: StateError('boom'));
// first-class functions
const add = _add; // a tear-off: a reference, not a call
final sub = (int a, int b) => a - b;
print(add(1, 2));
print(sub(5, 3));
// a function returning a function
int Function(int) multiplier(int factor) => (x) => x * factor;
final triple = multiplier(3);
print(triple(7));
}
int _add(int a, int b) => a + b;| Form | Declared as | Called as |
|---|---|---|
| Required positional | f(int a) | f(1) |
| Optional positional | f([int a = 0]) | f() or f(1) |
| Required named | f({required int a}) | f(a: 1) |
| Optional named | f({int a = 0}) | f() or f(a: 1) |
| Function parameter | f(void Function() cb) | f(() {}) |
| Typedef | typedef Handler = void Function(String) | f(handler) |
A default value must be a compile-time constant. When the natural default is an empty collection, use a const [] or a const {}, which is canonicalised and therefore safe to share.
Closures and captured variables
void main() {
// a closure captures the variable, not a snapshot of its value
var counter = 0;
void increment() => counter++; // sees later changes to counter
final adders = <int Function(int)>[];
for (var i = 0; i < 3; i++) {
adders.add((x) => x + i); // each closure captures its own i
}
print(adders.map((f) => f(100)).toList()); // [100, 101, 102]
// a function factory keeps state alive after the outer call returns
Counter makeCounter() {
var count = 0;
return Counter(() => ++count, () => count);
}
final c1 = makeCounter();
final c2 = makeCounter();
c1.bump();
print('${c1.value()} ${c2.value()}'); // 1 0: separate captured state
}
class Counter {
Counter(this.bump, this.value);
final int Function() bump;
final int Function() value;
}
// tear-offs bind the receiver: this is how you pass a method as a callback
class Greeter {
Greeter(this.prefix);
final String prefix;
String say(String name) => '$prefix $name';
}
void useIt() {
final g = Greeter('Hi');
const names = ['ada', 'alan'];
print(names.map(g.say).toList()); // no lambda needed
}- Dart's
forloop creates a fresh variable per iteration, so a closure capturing the loop variable behaves as expected. Awhileloop shares one variable and every closure sees the final value. - A captured variable keeps its enclosing scope alive. A long-lived callback holding a large object prevents that object from being collected.
- A tear-off of an instance method captures the receiver, so it is not a static function.
g.sayis a closure overg. - An immediately invoked function expression is idiomatic when you need a temporary scope:
(() { ... })().
Typedefs and function types
// a typedef names a function type, so signatures read as domain language
typedef Json = Map<String, Object?>;
typedef Validator = String? Function(String value);
typedef AsyncLoader<T> = Future<T> Function(String id);
class Field {
Field(this.name, this.validate);
final String name;
final Validator validate; // far clearer than the raw signature
String? check(String value) => validate(value);
}
class Repository {
Repository(this._loader);
final AsyncLoader<Json> _loader;
Future<Json> load(String id) => _loader(id);
}
void main() {
Validator notEmpty = (v) => v.trim().isEmpty ? 'required' : null;
Validator maxLength(int n) => (v) => v.length > n ? 'too long' : null;
final composite = (String v) => notEmpty(v) ?? maxLength(10)(v);
final form = Field('email', composite);
final long = List.filled(20, 'a').join();
for (final input in ['', long, '[email protected]']) {
print('$input -> ${form.check(input) ?? 'ok'}');
}
}💡
A typedef is documentation the compiler enforces. When the same function signature appears in three places, name it once: the call sites become shorter, the intent becomes obvious, and changing the signature becomes a single edit.
FAQ
Can I overload a function in Dart?
No, and there are no default parameters in the C++ sense. Use optional named parameters for variants and a factory or named constructor when the return type changes shape.
What is the difference between a function and a closure?
Every function in Dart is a closure. A top-level or static function captures nothing, which is why its tear-off can be a compile-time constant. A lambda captures whatever it references from the enclosing scope.
Related
Dart syntax and null safety Records, patterns and switch expressions
Last refreshed 2026-09-18.