Dart Functions — Parameters, Return Types, and Higher-Order Functions
In this tutorial, you will learn about Dart Functions. We cover key concepts, practical examples, and best practices to help you master this topic.
Dart functions are first-class objects that can be assigned to variables, passed as arguments, and returned from other functions, with support for optional parameters, type parameters, and concise arrow syntax.
What You Will Learn
- Declaring functions with parameters and return types
- Positional vs named vs optional parameters
- Arrow syntax for concise function bodies
- Anonymous functions and closures
- Higher-order functions that accept or return functions
- Lexical scoping and variable capture
- Using
typedeffor function type aliases
Why It Matters
Functions are the building blocks of readable, maintainable code. Dart's function system is designed to be expressive while remaining type-safe. Named parameters eliminate the need for overloaded functions. Arrow syntax reduces boilerplate for simple transformations. Higher-order functions enable Functional Programming patterns like map, filter, and reduce. Mastering functions is essential before moving to Flutter, where widgets are composed using callback functions extensively.
Real-World Use
In the DodaTech Flutter app, every screen uses callbacks for user interactions. Button onPressed handlers are anonymous functions, API callbacks use higher-order functions for success and error handling, and the state management layer uses function composition for middleware.
Learning Path
flowchart LR A[Control Flow] --> B[Dart Functions\nYou are here] B --> C[Dart Collections] style B fill:#f90,color:#fff
Basic Function Syntax
A Dart function consists of a return type, a name, a parameter list, and a body:
// Function with explicit return type and parameter types
int add(int a, int b) {
return a + b;
}
// Arrow syntax for single-expression functions
int multiply(int a, int b) => a * b;
// Void return type
void greet(String name) {
print('Hello, $name!');
}
void main() {
print('Add: ${add(5, 3)}');
print('Multiply: ${multiply(4, 6)}');
greet('Alice');
}
Output:
Add: 8
Multiply: 24
Hello, Alice!
Arrow syntax (=>) is preferred for functions that consist of a single expression. The return keyword is implicit with arrow syntax. Functions with multiple statements use block body with {} and explicit return.
Positional Parameters
Parameters are positional by default. Required parameters must be provided in order:
void main() {
printFullName('Alice', 'Johnson');
}
void printFullName(String first, String last) {
print('$first $last');
}
Output: Alice Johnson
All parameters are required unless marked optional. The compiler enforces that all required arguments are provided at the call site.
Optional Positional Parameters
Use square brackets to mark parameters as optional positional:
void main() {
introduce('Bob');
introduce('Carol', 25);
introduce('Dave', 30, 'New York');
}
void introduce(String name, [int? age, String? city]) {
var result = 'Hi, I am $name';
if (age != null) result += ', $age years old';
if (city != null) result += ', from $city';
print('$result.');
}
Output:
Hi, I am Bob.
Hi, I am Bob, 25 years old.
Hi, I am Bob, 30 years old, from New York.
Optional positional parameters must be nullable unless they have a default value. They are declared inside [] and appear after all required parameters.
Named Parameters
Named parameters use curly braces and can be provided in any order:
void main() {
// Named parameters in any order
createUser(
name: 'Alice',
age: 30,
email: 'alice@example.com',
);
// Omitting optional named parameters
createUser(
name: 'Bob',
email: 'bob@example.com', // age is omitted
);
}
void createUser({
required String name,
int? age,
required String email,
}) {
print('Created user: $name, $email');
if (age != null) print('Age: $age');
}
Output:
Created user: Alice, alice@example.com
Age: 30
Created user: Bob, bob@example.com
Use required for named parameters that must always be provided. Named parameters without required and without a default value are optional and nullable. Named parameters improve readability at call sites, especially for functions with many parameters.
Default Parameter Values
Both positional and named parameters can have default values:
void main() {
connectToServer();
connectToServer(port: 8080);
connectToServer(host: '192.168.1.1', port: 9090);
}
void connectToServer({
String host = 'localhost',
int port = 3000,
bool useTls = false,
}) {
print('Connecting to $host:$port (TLS: $useTls)');
}
Output:
Connecting to localhost:3000 (TLS: false)
Connecting to localhost:8080 (TLS: false)
Connecting to 192.168.1.1:9090 (TLS: false)
Default values must be compile-time constants. They are evaluated once at the call site, not once per function declaration.
Anonymous Functions
Anonymous functions (lambdas) are functions without a name. They are commonly used as callbacks:
void main() {
// Anonymous function assigned to a variable
var square = (int x) => x * x;
print('Square of 7: ${square(7)}');
// Anonymous function as a callback
var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map((n) => n * 2).toList();
print('Doubled: $doubled');
// Multi-line anonymous function
numbers.forEach((n) {
var squared = n * n;
print('$n squared is $squared');
});
}
Output:
Square of 7: 49
Doubled: [2, 4, 6, 8, 10]
1 squared is 1
2 squared is 4
3 squared is 9
...
Anonymous functions have the same type system as named functions. The parameter types can be inferred from context, making the syntax concise.
Higher-Order Functions
A higher-order function is a function that takes another function as a parameter, returns a function, or both:
void main() {
var numbers = [1, 2, 3, 4, 5, 6];
// filter: keeps elements matching a predicate
var evens = numbers.where((n) => n.isEven).toList();
print('Evens: $evens');
// map: transforms each element
var strings = numbers.map((n) => 'Number: $n').toList();
print(strings);
// reduce: combines elements into a single value
var sum = numbers.reduce((a, b) => a + b);
print('Sum: $sum');
// Custom higher-order function
var result = applyOperation(10, 5, (a, b) => a * b);
print('Operation result: $result');
}
int applyOperation(int a, int b, int Function(int, int) operation) {
return operation(a, b);
}
Output:
Evens: [2, 4, 6]
[Number: 1, Number: 2, Number: 3, Number: 4, Number: 5, Number: 6]
Sum: 21
Operation result: 50
The where, map, and reduce methods on collections are built-in higher-order functions. Custom higher-order functions accept callbacks using Function types or typedef.
Closures
A closure is a function that captures variables from its surrounding lexical scope:
void main() {
// makeCounter returns a closure
var counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
print(counter()); // 3
// Each closure has its own captured state
var anotherCounter = makeCounter();
print(anotherCounter()); // 1 (independent)
}
Function makeCounter() {
int count = 0;
return () {
count++;
return count;
};
}
Output:
1
2
3
1
The closure captures the count variable. Even though makeCounter has returned, the closure maintains access to count. Each call to makeCounter() creates a new count variable that is independent of others.
Typedef
typedef creates a type alias for function types, making signatures easier to read:
// Define a function type
typedef IntOperation = int Function(int a, int b);
int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;
int compute(int a, int b, IntOperation op) {
print('Computing with $a and $b');
return op(a, b);
}
void main() {
print(compute(10, 5, add)); // 15
print(compute(10, 5, subtract)); // 5
print(compute(10, 5, (a, b) => a * b)); // 50
}
Output:
Computing with 10 and 5
15
Computing with 10 and 5
5
Computing with 10 and 5
50
Without typedef, the compute function would need int Function(int, int) op as the parameter type. typedef makes the intent clearer and reduces duplication.
Generator Functions
Generator functions produce a sequence of values lazily using sync* and yield:
void main() {
var numbers = countUpTo(5);
for (var n in numbers) {
print('Generated: $n');
}
}
Iterable<int> countUpTo(int max) sync* {
int i = 1;
while (i <= max) {
yield i;
i++;
}
}
Output:
Generated: 1
Generated: 2
Generated: 3
Generated: 4
Generated: 5
The sync* keyword marks a function as a synchronous generator. yield produces each value. The iteration is lazy: values are computed only when requested.
Common Mistakes
Omitting
requiredfor named parameters that should be mandatory: Named parameters are optional by default. Addrequiredto ensure callers provide the parameter.Using
Functionwithout type parameters:Functionaccepts any function signature. Use typed versions likevoid Function(String)ortypedeffor type safety.Confusing
=>with=>as a comparison: The arrow=>is not a comparison operator. It defines the function body. Comparison is==,>=,<=.Modifying captured variables in closures unintentionally: Closures capture variables by reference. Changing a captured variable in one closure affects all closures that captured the same variable.
Not using
requiredwith named parameters for constructors: Constructor parameters should userequiredor default values. Omitting both makes the parameter nullable and creates the possibility of incomplete object initialization.
Practice Questions
- What is the difference between positional and named parameters?
- How does arrow syntax differ from block body syntax?
- What is a closure and what does it capture?
- How does
sync*differ from a regular function that returns a List? - Challenge: Implement a
createValidatorfunction that takes a validation rule (a function from String to String?) and returns a function that validates a list of strings. The returned function should collect all validation errors and return them as a list.
Mini Project
Build a string processing library:
- A
transformhigher-order function that accepts a string and a transformation function - Pre-defined transformations: uppercase, lowercase, reverse, capitalize
- A
pipelinefunction that chains multiple transformations - A
filterfunction that keeps strings matching a predicate - Use
typedeffor common function signatures - Write unit tests for each function
FAQ
What is Next
Now that you understand functions, learn how to work with data collections. Proceed to Collections in Dart for lists, sets, maps, and collection operations. Then explore Classes in Dart for object-oriented programming.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro