Intermediate

Functions in Dart

Learn how to define and use functions in Dart - parameters, named and optional arguments, arrow syntax, closures, recursion, and higher-order functions

Functions are the building blocks of any Dart program. In Dart, functions are first-class objects — they can be assigned to variables, passed as arguments, and returned from other functions. This is a direct consequence of Dart’s multi-paradigm design: it borrows object-oriented structure from Java and C#, but its treatment of functions comes straight from the functional tradition.

What makes Dart’s function model especially interesting is its parameter system. Most languages give you either positional parameters or keyword/named parameters, but Dart gives you both — plus optional positional parameters, default values, and required named parameters. Combined with sound null safety, this lets you write APIs that are both flexible and safe at compile time.

In this tutorial you’ll learn how to define functions, work with Dart’s three kinds of parameters, use the concise arrow syntax, capture state with closures, write recursive functions, and treat functions as values with higher-order functions. By the end you’ll understand why Dart code — and especially Flutter code — relies so heavily on passing functions around.

Defining and Calling Functions

A Dart function declares its return type, a name, a parameter list, and a body. If a function does not return a value, its return type is void. Dart also infers return types, but being explicit is idiomatic for top-level functions.

Create a file named functions_basic.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// A function with a typed parameter and a typed return value
int square(int n) {
  return n * n;
}

// A void function performs an action but returns nothing
void printBanner(String text) {
  print('=== $text ===');
}

void main() {
  printBanner('Functions');
  print('square(5) = ${square(5)}');
  print('square(square(3)) = ${square(square(3))}');
}

Functions can be called anywhere they are in scope, and their results can be nested directly inside other expressions, as square(square(3)) shows.

Arrow Functions and Parameters

For functions whose body is a single expression, Dart offers the => “arrow” syntax — => expr is shorthand for { return expr; }. Dart also supports three styles of parameters:

  • Required positional — the default; order matters.
  • Optional positional — wrapped in [ ], may be omitted.
  • Named — wrapped in { }, passed by name; mark them required or give them a default.

Create a file named functions_params.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Arrow function: a single-expression body
int cube(int n) => n * n * n;

// Optional positional parameters use [ ] and need a default or nullable type
String greet(String name, [String greeting = 'Hello']) {
  return '$greeting, $name!';
}

// Named parameters use { }. 'required' forces the caller to provide one;
// others can have defaults.
double price({required double base, double taxRate = 0.0}) {
  return base + (base * taxRate);
}

void main() {
  print('cube(3) = ${cube(3)}');

  // Optional positional parameter omitted, then provided
  print(greet('World'));
  print(greet('Dart', 'Welcome'));

  // Named parameters can appear in any order
  print('Total: ${price(base: 100.0, taxRate: 0.08)}');
  print('Total: ${price(taxRate: 0.2, base: 50.0)}');
}

Named parameters make call sites self-documenting — price(base: 100.0, taxRate: 0.08) reads clearly without checking the function signature. This is why Flutter widget constructors use named parameters almost everywhere.

Scope, Closures, and Nested Functions

Dart uses lexical scoping: an inner function can see variables declared in the functions that enclose it. When a nested function captures and remembers those variables even after the outer function returns, it forms a closure. Because functions are objects, a closure can be returned and called later.

Create a file named functions_closures.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// makeCounter returns a closure that captures its own 'count' variable.
// Each returned counter keeps its own independent state.
int Function() makeCounter() {
  int count = 0;
  return () {
    count++;
    return count;
  };
}

void main() {
  var counterA = makeCounter();
  var counterB = makeCounter();

  print('A: ${counterA()}');  // 1
  print('A: ${counterA()}');  // 2
  print('A: ${counterA()}');  // 3
  print('B: ${counterB()}');  // 1 - independent state

  // Demonstrate local scope: 'secret' is only visible inside main
  var secret = 42;
  void reveal() => print('The secret is $secret');
  reveal();
}

The type int Function() is the type of “a function that takes no arguments and returns an int” — Dart has full first-class function types. Notice that counterA and counterB each carry their own count; closures capture variables, not values.

Recursion

A recursive function calls itself to solve a problem in terms of smaller subproblems. Dart handles recursion like any C-style language, using the call stack. The classic example is factorial, along with the Fibonacci sequence.

Create a file named functions_recursion.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Factorial: n! = n * (n-1)!  with a base case of 0! = 1
int factorial(int n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
}

// Fibonacci: each number is the sum of the two before it
int fibonacci(int n) {
  if (n < 2) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

void main() {
  print('5! = ${factorial(5)}');
  print('10! = ${factorial(10)}');

  // Print the first 10 Fibonacci numbers
  var sequence = <int>[];
  for (var i = 0; i < 10; i++) {
    sequence.add(fibonacci(i));
  }
  print('Fibonacci: $sequence');
}

Each call to factorial waits for the smaller call to finish before multiplying, until the base case n <= 1 stops the recursion.

Higher-Order Functions

Because functions are first-class objects, they can be passed as arguments and returned as results. Functions that do this are called higher-order functions. Dart’s collection types lean heavily on them through methods like map, where, and reduce, and you can also pass anonymous (lambda) functions inline.

Create a file named functions_higher_order.dart:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// A higher-order function: it takes another function as a parameter.
List<int> applyToAll(List<int> items, int Function(int) transform) {
  return items.map(transform).toList();
}

// A function that returns a function (a function factory)
int Function(int) multiplier(int factor) {
  return (int x) => x * factor;
}

void main() {
  var numbers = [1, 2, 3, 4, 5];

  // Pass a named function as an argument
  int double(int n) => n * 2;
  print('Doubled: ${applyToAll(numbers, double)}');

  // Pass an anonymous function (lambda) directly
  print('Squared: ${applyToAll(numbers, (n) => n * n)}');

  // Build a specialized function and use it
  var triple = multiplier(3);
  print('Tripled: ${applyToAll(numbers, triple)}');

  // Built-in higher-order methods
  var evens = numbers.where((n) => n.isEven).toList();
  var sum = numbers.reduce((a, b) => a + b);
  print('Evens: $evens');
  print('Sum: $sum');
}

The (n) => n * n syntax is an anonymous function — a function with no name passed directly where a value is expected. This style is everywhere in idiomatic Dart, from list processing to Flutter event callbacks.

Running with Docker

You can run every example with the official Dart image — no local SDK required.

1
2
3
4
5
6
7
8
9
# Pull the official image
docker pull dart:stable

# Run each example
docker run --rm -v $(pwd):/app -w /app dart:stable dart run functions_basic.dart
docker run --rm -v $(pwd):/app -w /app dart:stable dart run functions_params.dart
docker run --rm -v $(pwd):/app -w /app dart:stable dart run functions_closures.dart
docker run --rm -v $(pwd):/app -w /app dart:stable dart run functions_recursion.dart
docker run --rm -v $(pwd):/app -w /app dart:stable dart run functions_higher_order.dart

Expected Output

Running functions_basic.dart:

=== Functions ===
square(5) = 25
square(square(3)) = 81

Running functions_params.dart:

cube(3) = 27
Hello, World!
Welcome, Dart!
Total: 108.0
Total: 60.0

Running functions_closures.dart:

A: 1
A: 2
A: 3
B: 1
The secret is 42

Running functions_recursion.dart:

5! = 120
10! = 3628800
Fibonacci: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Running functions_higher_order.dart:

Doubled: [2, 4, 6, 8, 10]
Squared: [1, 4, 9, 16, 25]
Tripled: [3, 6, 9, 12, 15]
Evens: [2, 4]
Sum: 15

Key Concepts

  • Functions are first-class objects — they can be stored in variables, passed as arguments, and returned, with types written as ReturnType Function(ArgTypes).
  • Arrow syntax=> expr is concise shorthand for a single-expression body that returns expr.
  • Three parameter styles — required positional, optional positional in [ ], and named in { }; named parameters can be required or carry default values.
  • Named parameters self-document call sites, which is why Flutter constructors rely on them so heavily.
  • Closures capture variables, not values — each closure keeps its own live reference to the enclosing scope’s variables.
  • Recursion works through the call stack and needs a base case to terminate; factorial and Fibonacci are canonical examples.
  • Higher-order functions power Dart’s collection pipeline (map, where, reduce) and its callback-driven UI code.
  • Anonymous functions(args) => body or (args) { ... } — let you define behavior inline exactly where it’s needed.

Running Today

All examples can be run using Docker:

docker pull dart:stable
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining