Dart Programming Fundamentals

Functions & Parameters

40 min Lesson 8 of 13

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.

Note: In Dart, even the 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!
}
Tip: Although Dart can infer return types, it is considered best practice to always explicitly declare the return type for clarity and maintainability.

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
}
Warning: You cannot mix optional positional parameters [] 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!
}
Tip: Arrow functions are perfect for simple getters, callbacks, and short utility functions. Avoid using them for complex logic that requires multiple statements.

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
}
Note: Closures are powerful because they "remember" the environment in which they were created. This makes them useful for callbacks, event handlers, and creating function factories.

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
}
Warning: Recursive functions can cause a stack overflow if the base case is never reached or if the input is too large. Always ensure your recursion terminates. For large inputs, consider using iteration instead.

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
}
Tip: Keep your variables in the narrowest scope possible. This prevents naming conflicts, makes code easier to reason about, and helps the garbage collector free memory sooner.

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:

  1. A function power that takes a base (required) and an optional named parameter exponent with a default value of 2. It should use recursion to calculate the result.
  2. A higher-order function applyToAll that takes a List<int> and a function, then returns a new list with the function applied to each element.
  3. A function createRangeChecker that takes min and max values 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