← Back to blog
Dart Fundamentals · Part 7 of 10
July 5, 20269 min read

Functions in Dart: Named, Optional & Positional Params, Arrow Syntax

DartFlutter

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.