Functions & Parameters
Functions & Parameters in Dart
Functions are the building blocks of any Dart application. They allow you to organize code into reusable, modular pieces that perform specific tasks. In this lesson, we will explore everything from basic function definitions to advanced concepts like closures and higher-order functions.
main() function is a top-level function. Dart is a true object-oriented language, but functions can exist outside of classes as top-level functions.
Defining Functions
A function in Dart is defined by specifying a return type, a name, parameters in parentheses, and a body enclosed in curly braces.
// Basic function with a return type
String greet(String name) {
return 'Hello, $name!';
}
// Function with no return value (void)
void printMessage(String message) {
print(message);
}
// Calling the functions
void main() {
String greeting = greet('Alice');
print(greeting); // Hello, Alice!
printMessage('Welcome to Dart!'); // Welcome to Dart!
}
Return Types
Every function in Dart has a return type. If no return type is specified, Dart implicitly treats it as dynamic. Common return types include void, String, int, double, bool, List, Map, and custom types.
// Returns an integer
int add(int a, int b) {
return a + b;
}
// Returns a boolean
bool isEven(int number) {
return number % 2 == 0;
}
// Returns a List
List<String> getNames() {
return ['Alice', 'Bob', 'Charlie'];
}
// Returns a Map
Map<String, int> getScores() {
return {'Alice': 95, 'Bob': 87, 'Charlie': 92};
}
void main() {
print(add(3, 5)); // 8
print(isEven(4)); // true
print(getNames()); // [Alice, Bob, Charlie]
print(getScores()); // {Alice: 95, Bob: 87, Charlie: 92}
}
Required Parameters
By default, all positional parameters in Dart are required. The function cannot be called without providing values for each required parameter.
// Both parameters are required
double calculateArea(double width, double height) {
return width * height;
}
void main() {
double area = calculateArea(5.0, 3.0);
print('Area: $area'); // Area: 15.0
// calculateArea(5.0); // ERROR: Too few positional arguments
}
Optional Positional Parameters
Optional positional parameters are enclosed in square brackets []. They can be omitted when calling the function, in which case they default to null (or a specified default value).
String buildGreeting(String name, [String? title, String? suffix]) {
String result = 'Hello, ';
if (title != null) {
result += '$title ';
}
result += name;
if (suffix != null) {
result += ' $suffix';
}
return result;
}
void main() {
print(buildGreeting('Smith')); // Hello, Smith
print(buildGreeting('Smith', 'Dr.')); // Hello, Dr. Smith
print(buildGreeting('Smith', 'Dr.', 'PhD')); // Hello, Dr. Smith PhD
}
Optional Named Parameters
Named parameters are enclosed in curly braces {}. They are optional by default and are referenced by name when calling the function. This improves code readability significantly.
void createUser({
required String name,
required String email,
int age = 0,
String role = 'user',
}) {
print('Name: $name');
print('Email: $email');
print('Age: $age');
print('Role: $role');
}
void main() {
createUser(
name: 'Alice',
email: 'alice@example.com',
);
// Name: Alice
// Email: alice@example.com
// Age: 0
// Role: user
createUser(
name: 'Bob',
email: 'bob@example.com',
age: 30,
role: 'admin',
);
// Name: Bob
// Email: bob@example.com
// Age: 30
// Role: admin
}
[] and optional named parameters {} in the same function. You must choose one or the other.
Default Values
Both optional positional and named parameters can have default values. If the caller does not provide a value, the default is used.
// Default values with optional positional parameters
String repeat(String text, [int times = 1, String separator = ' ']) {
return List.filled(times, text).join(separator);
}
// Default values with named parameters
double calculatePrice({
required double basePrice,
double taxRate = 0.10,
double discount = 0.0,
}) {
double taxed = basePrice * (1 + taxRate);
return taxed - discount;
}
void main() {
print(repeat('Hi')); // Hi
print(repeat('Hi', 3)); // Hi Hi Hi
print(repeat('Hi', 3, '-')); // Hi-Hi-Hi
print(calculatePrice(basePrice: 100.0)); // 110.0
print(calculatePrice(basePrice: 100.0, discount: 10.0)); // 100.0
print(calculatePrice(basePrice: 100.0, taxRate: 0.20)); // 120.0
}
Arrow Functions (=>)
For functions that contain a single expression, Dart provides a shorthand syntax using the fat arrow (=>). The expression is evaluated and returned automatically.
// Traditional function
int addTraditional(int a, int b) {
return a + b;
}
// Arrow function equivalent
int addArrow(int a, int b) => a + b;
// Arrow functions with various return types
bool isAdult(int age) => age >= 18;
String capitalize(String s) => s[0].toUpperCase() + s.substring(1);
double circleArea(double radius) => 3.14159 * radius * radius;
// Arrow function returning void (performs an action)
void sayHello(String name) => print('Hello, $name!');
void main() {
print(addArrow(3, 5)); // 8
print(isAdult(20)); // true
print(capitalize('dart')); // Dart
print(circleArea(5.0)); // 78.53975
sayHello('World'); // Hello, World!
}
Anonymous Functions (Closures)
Anonymous functions (also called lambdas or closures) are functions without a name. They are commonly used as arguments to other functions, especially with collection methods.
void main() {
// Anonymous function assigned to a variable
var multiply = (int a, int b) {
return a * b;
};
print(multiply(4, 5)); // 20
// Anonymous arrow function
var square = (int n) => n * n;
print(square(6)); // 36
// Anonymous functions with List methods
var numbers = [1, 2, 3, 4, 5];
// forEach with anonymous function
numbers.forEach((number) {
print('Number: $number');
});
// map with anonymous arrow function
var doubled = numbers.map((n) => n * 2).toList();
print(doubled); // [2, 4, 6, 8, 10]
// where (filter) with anonymous function
var evens = numbers.where((n) => n % 2 == 0).toList();
print(evens); // [2, 4]
// reduce with anonymous function
var sum = numbers.reduce((a, b) => a + b);
print(sum); // 15
}
Closures and Variable Capture
A closure is a function that captures variables from its surrounding scope. The closure retains access to these variables even after the enclosing function has returned.
// A function that returns a closure
Function makeCounter() {
int count = 0;
return () {
count++;
return count;
};
}
// A closure that captures a multiplier
Function makeMultiplier(int factor) {
return (int number) => number * factor;
}
void main() {
var counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
print(counter()); // 3
var doubler = makeMultiplier(2);
var tripler = makeMultiplier(3);
print(doubler(5)); // 10
print(tripler(5)); // 15
}
Higher-Order Functions
A higher-order function is a function that takes other functions as parameters or returns a function. Dart fully supports higher-order functions, making it a great language for functional-style programming.
// A higher-order function that takes a function as a parameter
int applyOperation(int a, int b, int Function(int, int) operation) {
return operation(a, b);
}
// A higher-order function that returns a function
Function createGreeter(String greeting) {
return (String name) => '$greeting, $name!';
}
// Using typedef for function types
typedef MathOperation = int Function(int, int);
int calculate(int a, int b, MathOperation op) {
return op(a, b);
}
void main() {
// Passing functions as arguments
print(applyOperation(10, 5, (a, b) => a + b)); // 15
print(applyOperation(10, 5, (a, b) => a - b)); // 5
print(applyOperation(10, 5, (a, b) => a * b)); // 50
// Using a returned function
var helloGreeter = createGreeter('Hello');
var hiGreeter = createGreeter('Hi');
print(helloGreeter('Alice')); // Hello, Alice!
print(hiGreeter('Bob')); // Hi, Bob!
// Using typedef
MathOperation add = (a, b) => a + b;
MathOperation multiply = (a, b) => a * b;
print(calculate(6, 3, add)); // 9
print(calculate(6, 3, multiply)); // 18
}
Recursion Basics
Recursion is when a function calls itself. Every recursive function needs a base case (a condition that stops the recursion) and a recursive case (where the function calls itself with a smaller problem).
// Factorial using recursion
int factorial(int n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1); // Recursive case
}
// Fibonacci sequence using recursion
int fibonacci(int n) {
if (n <= 0) return 0; // Base case
if (n == 1) return 1; // Base case
return fibonacci(n - 1) + fibonacci(n - 2); // Recursive case
}
// Sum of a list using recursion
int sumList(List<int> numbers) {
if (numbers.isEmpty) return 0;
return numbers.first + sumList(numbers.sublist(1));
}
void main() {
print(factorial(5)); // 120 (5 * 4 * 3 * 2 * 1)
print(fibonacci(7)); // 13 (0, 1, 1, 2, 3, 5, 8, 13)
print(sumList([1, 2, 3, 4, 5])); // 15
}
Scope
Scope determines where variables are accessible. Dart uses lexical scoping, meaning a variable is available within the block of code where it is defined, including any nested blocks.
// Top-level (global) scope
String globalVar = 'I am global';
void outerFunction() {
// Local scope
String outerVar = 'I am in outerFunction';
void innerFunction() {
// Nested local scope
String innerVar = 'I am in innerFunction';
// Can access all outer scopes
print(globalVar); // I am global
print(outerVar); // I am in outerFunction
print(innerVar); // I am in innerFunction
}
innerFunction();
print(globalVar); // I am global
print(outerVar); // I am in outerFunction
// print(innerVar); // ERROR: innerVar is not accessible here
}
void main() {
outerFunction();
print(globalVar); // I am global
// print(outerVar); // ERROR: outerVar is not accessible here
// Block scope with if/for
for (int i = 0; i < 3; i++) {
var loopVar = 'Iteration $i';
print(loopVar);
}
// print(loopVar); // ERROR: loopVar is not accessible outside the loop
}
Putting It All Together
Here is a comprehensive example that combines multiple function concepts into a practical scenario.
// typedef for clarity
typedef Validator = bool Function(String);
// Higher-order function that validates data
List<String> filterValid(List<String> items, Validator validator) {
return items.where(validator).toList();
}
// Functions that return validators (closures)
Validator minLength(int min) {
return (String value) => value.length >= min;
}
Validator containsChar(String char) {
return (String value) => value.contains(char);
}
// Named parameters with defaults
String formatList(
List<String> items, {
String separator = ', ',
String prefix = '',
String suffix = '',
}) {
return prefix + items.join(separator) + suffix;
}
void main() {
var emails = [
'alice@example.com',
'bob',
'charlie@test.com',
'd@e',
'frank@example.com',
];
// Use closures as validators
var validEmails = filterValid(emails, (email) {
return minLength(5)(email) && containsChar('@')(email);
});
print(formatList(
validEmails,
prefix: 'Valid emails: [',
suffix: ']',
));
// Valid emails: [alice@example.com, charlie@test.com, frank@example.com]
}
Practice Exercise
Create the following functions:
- A function
powerthat takes abase(required) and an optional named parameterexponentwith a default value of 2. It should use recursion to calculate the result. - A higher-order function
applyToAllthat takes aList<int>and a function, then returns a new list with the function applied to each element. - A function
createRangeCheckerthat takesminandmaxvalues and returns a closure that checks whether a given number is within that range.
Expected output:
// power(3) should return 9 (3^2)
// power(2, exponent: 5) should return 32 (2^5)
// applyToAll([1, 2, 3], (n) => n * 10) should return [10, 20, 30]
// var check = createRangeChecker(1, 10);
// check(5) should return true
// check(15) should return false