Dart Lesson 6: Advanced Functions – Higher-Order Functions and Closures



This content originally appeared on DEV Community and was authored by Ge Ji

Today, we’ll dive into advanced function features — higher-order functions and closures. These are core concepts in Dart’s functional programming paradigm, making code more concise and flexible, and are essential techniques in Flutter development.

I. Higher-Order Functions: Functions that Work with Other Functions

A higher-order function is a function that can accept other functions as parameters or return a function as a result. Simply put, it’s a function that “operates” on other functions.

1. Functions as Parameters

When functions are passed as parameters, we can abstract common logic, making functions more extensible.

Example: Generic Calculation Framework

// Higher-order function: accepts two numbers and an operation function
void calculate(int a, int b, int Function(int, int) operation) {
  int result = operation(a, b);
  print("Calculation result: $result");
}

// Define specific specific operation functions
int add(int x, int y) => x + y;
int multiply(int x, int y) => x * y;

void main() {
  // Pass the addition function
  calculate(3, 4, add); // Output: Calculation result: 7

  // Pass the multiplication function (or directly pass an anonymous function)
  calculate(3, 4, multiply); // Output: Calculation result: 12
  calculate(3, 4, (x, y) => x - y); // Output: Calculation result: -1
}

In this example, calculate is a higher-order function. It doesn’t care about the specific operation logic, only about executing the passed operation function. This pattern is common in framework design.

2. Functions as Return Values

When functions return other functions, we can dynamically generate functions with specific behaviors.

Example: Generating Custom Calculators

// Higher-order function: returns corresponding operation function based on operator
Function getCalculator(String operator) {
  switch (operator) {
    case "+":
      return (int a, int b) => a + b;
    case "-":
      return (int a, int b) => a - b;
    case "*":
      return (int a, int b) => a * b;
    case "/":
      return (int a, int b) => a ~/ b; // Integer division
    default:
      return (int a, int b) => 0;
  }
}

void main() {
  // Get an addition calculator
  var adder = getCalculator("+");
  print(adder(5, 3)); // Output: 8

  // Get a multiplication calculator
  var multiplier = getCalculator("*");
  print(multiplier(5, 3)); // Output: 15
}

Here, getCalculator dynamically returns different operation functions based on the input operator, achieving the effect of “function on demand”.


II. Common Higher-Order Functions: forEach/map/reduce

Dart’s collection classes (List, Set, etc.) have many built-in higher-order functions that simplify data processing.

1. forEach: Iterating Over Collections

forEach accepts a function parameter and iterates over each element in the collection (which we touched on in the previous lesson).

void main() {
  List<String> languages = ['Dart', 'Flutter', 'Java'];

  // Iterate and print each element
  languages.forEach((lang) {
    print("Language: $lang");
  });
  // Output:
  // Language: Dart
  // Language: Flutter
  // Language: Java
}

2. map: Transforming Collection Elements

map accepts a transformation function, converts each element in the collection according to rules, and returns a new iterable (usually converted to a list with toList()).

Example: Data Transformation

void main() {
  List<int> numbers = [1, 2, 3, 4];

  // Convert each number to its square
  var squared = numbers.map((n) => n * n);
  print(squared.toList()); // Output: [1, 4, 9, 16]

  // Convert integer list to string list
  var numberStrings = numbers.map((n) => "Number: $n");
  print(
    numberStrings.toList(),
  ); // Output: [Number: 1, Number: 2, Number: 3, Number: 4]
}

3. reduce: Aggregating Collection Elements

reduce accepts an aggregation function and combines elements in the collection into a single result (starting with the first element and progressively combining with the next).

Example: Summing and Multiplying

void main() {
  List<int> numbers = [1, 2, 3, 4, 5];

  // Sum: ((((1+2)+3)+4)+5) = 15
  int sum = numbers.reduce((value, element) => value + element);
  print("Sum: $sum"); // Output: Sum: 15

  // Product: ((((1*2)*3)*4)*5) = 120
  int product = numbers.reduce((value, element) => value * element);
  print("Product: $product"); // Output: Product: 120
}

4. Chaining: Combining Higher-Order Functions

These higher-order functions can be chained together to implement complex data processing logic:

void main() {
  List<int> scores = [85, 92, 78, 65, 90];

  // Step 1: Filter scores greater than 80
  // Step 2: Convert scores to strings like "Excellent: XX"
  // Step 3: Iterate and print results
  scores
      .where((s) => s > 80) // Filter: [85, 92, 90]
      .map(
        (s) => "Excellent: $s",
      ) // Transform: ["Excellent: 85", "Excellent: 92", "Excellent: 90"]
      .forEach((s) => print(s)); // Iterate and print

  // Output:
  // Excellent: 85
  // Excellent: 92
  // Excellent: 90
}

This chaining style is very concise, avoiding verbose loops and temporary variables.


III. Closures: “Binding” Between Functions and Variables

A closure is a structure formed when an inner function in a nested function references variables from the outer function, and the inner function is returned or passed outside. Simply put, it’s a function that “remembers” the environment in which it was created.

1. Basic Form of a Closure

// Outer function
Function makeCounter() {
  int count = 0; // Local variable of outer function

  // Inner function (closure): references outer count variable
  int increment() {
    count++;
    return count;
  }

  return increment; // Return inner function
}

void main() {
  // Get closure function
  var counter = makeCounter();

  // Calling multiple times shows count is "remembered"
  print(counter()); // Output: 1
  print(counter()); // Output: 2
  print(counter()); // Output: 3
}

In this example, the increment function is a closure. It references the count variable from the outer function makeCounter. Even after makeCounter finishes executing, count isn’t destroyed but is “preserved” by the increment function.

2. Core Feature of Closures: Variable Isolation

Each closure maintains its own independent variable environment, without interference:

Function makeCounter() {
  int count = 0;
  return () {
    count++;
    return count;
  };
}

void main() {
  var counter1 = makeCounter();
  var counter2 = makeCounter();

  print(counter1()); // 1 (counter1's count)
  print(counter1()); // 2

  print(counter2()); // 1 (counter2's count, independent of counter1)
  print(counter2()); // 2
}

counter1 and counter2 are two independent closures with their own count variables that don’t affect each other, achieving variable isolation.

3. Practical Use Cases for Closures

  • Encapsulating private variables: Dart doesn’t have a private keyword, but closures can simulate private variables.
  • Caching calculation results: Storing results of expensive calculations to avoid redundant computations.
  • Event handling: Preserving context information in callback functions.

Example: Simulating Private Variables

class User {
  // Implement "private" password variable with closure
  final _password = (() {
    String _secret = "123456"; // Not directly accessible from outside
    return {'get': () => _secret, 'set': (String newPwd) => _secret = newPwd};
  })();

  String name;

  User(this.name);

  // Only access "private" password through methods
  String getPassword() => _password['get']!();
  void setPassword(String newPwd) => _password['set']!(newPwd);
}

void main() {
  User user = User("Zhang San");
  print(user.getPassword()); // Output: 123456

  user.setPassword("abcdef");
  print(user.getPassword()); // Output: abcdef

  // Cannot directly access _secret (compilation error)
  // print(user._secret);
}

IV. Comprehensive Example with Higher-Order Functions and Closures

Let’s use a “shopping cart calculation” example to integrate what we’ve learned:

void main() {
  // Shopping cart items: [name, price, quantity]
  List<List<dynamic>> cart = [
    ['Apple', 5.99, 2],
    ['Banana', 3.99, 3],
    ['Orange', 4.50, 1],
  ];

  // 1. Calculate total price for each item (price * quantity) - using map
  var itemTotals = cart.map((item) => item[1] * item[2]);

  // 2. Calculate total cart price - using reduce
  double total = itemTotals.reduce((sum, price) => sum + price);

  // 3. Create a price calculator with discount (closure)
  Function createDiscountCalculator(double discount) {
    return (double price) => price * (1 - discount);
  }

  // 4. Calculate final price with 10% discount
  var discount90 = createDiscountCalculator(0.1);
  double finalTotal = discount90(total);

  print(
    "Original price: ${total.toStringAsFixed(2)}",
  ); // Output: Original price: 28.45
  print(
    "After 10% discount: ${finalTotal.toStringAsFixed(2)}",
  ); // Output: After 10% discount: 25.61
}

In this example, we used map for data transformation, reduce for aggregation, and a closure to create a stateful discount calculator, demonstrating the flexibility of higher-order functions and closures.


This content originally appeared on DEV Community and was authored by Ge Ji