6  Functions

Functions are essential building blocks in Dart, just like in other programming languages. Given your experience with Python, you’ll find many similarities but also some important differences in how Dart handles functions. Let’s explore the fundamentals of Dart functions.

6.1 Basic Function Declaration

In Dart, you declare functions by specifying a return type, function name, parameters, and a body:

ReturnType functionName(ParameterType parameter) {
  // Function body
  return value;
}

Here’s a simple example of a function that adds two numbers:

int add(int a, int b) {
  return a + b;
}

void main() {
  print(add(5, 3)); // Output: 8
}

Notice how unlike Python, Dart requires: - Type declarations for parameters and return values - Curly braces {} to define the function body - Semicolons ; at the end of statements - The return keyword to specify what value the function returns

6.2 Void Functions

If a function doesn’t return a value, you can use the void keyword:

void printGreeting(String name) {
  print('Hello, $name!');
}

void main() {
  printGreeting('Kittipos'); // Output: Hello, Kittipos!
}

6.3 Function Parameters

Dart offers several ways to work with parameters:

6.3.1 Required Parameters

These are the standard parameters that must be provided when calling a function:

String createFullName(String firstName, String lastName) {
  return '$firstName $lastName';
}

void main() {
  print(createFullName('John', 'Doe')); // Output: John Doe
}

6.3.2 Optional Positional Parameters

You can make parameters optional by wrapping them in square brackets []:

String createFullName(String firstName, [String? lastName]) {
  if (lastName != null) {
    return '$firstName $lastName';
  }
  return firstName;
}

void main() {
  print(createFullName('John', 'Doe')); // Output: John Doe
  print(createFullName('John')); // Output: John
}

Notice the use of String? which is part of Dart’s null safety feature, indicating that lastName can be null.

6.3.3 Optional Named Parameters

Named parameters make function calls more readable. Wrap them in curly braces {}:

String createFullName(String firstName, {String? lastName}) {
  if (lastName != null) {
    return '$firstName $lastName';
  }
  return firstName;
}

void main() {
  print(createFullName('John', lastName: 'Doe')); // Output: John Doe
  print(createFullName('John')); // Output: John
}

6.3.4 Default Parameter Values

You can provide default values for optional parameters:

String createFullName(String firstName, {String lastName = 'Unknown'}) {
  return '$firstName $lastName';
}

void main() {
  print(createFullName('John')); // Output: John Unknown
  print(createFullName('John', lastName: 'Doe')); // Output: John Doe
}

6.3.5 Required Named Parameters

In Dart, you can also make named parameters required using the required keyword:

String createFullName({required String firstName, required String lastName}) {
  return '$firstName $lastName';
}

void main() {
  print(createFullName(firstName: 'John', lastName: 'Doe')); // Output: John Doe
  // print(createFullName(firstName: 'John')); // Error: Missing required parameter 'lastName'
}

6.4 Arrow Functions (Lambda Expressions)

For simple functions that just return an expression, you can use the arrow syntax (=>), similar to lambda functions in Python:

// Regular function
int add(int a, int b) {
  return a + b;
}

// Arrow function equivalent
int addArrow(int a, int b) => a + b;

void main() {
  print(add(5, 3)); // Output: 8
  print(addArrow(5, 3)); // Output: 8
}

6.5 Anonymous Functions

Dart supports anonymous functions (functions without names), which are useful for one-time use or as arguments to other functions:

void main() {
  var numbers = [1, 2, 3, 4, 5];
  
  // Using an anonymous function with forEach
  numbers.forEach((number) {
    print(number * 2);
  });
  
  // Using an arrow function
  numbers.forEach((number) => print(number * 3));
  
  // Even shorter with tear-off
  numbers.forEach(print);
}

6.6 Higher-Order Functions

Dart supports higher-order functions, which are functions that take other functions as parameters or return functions:

Function makeMultiplier(int multiplier) {
  // This function returns another function
  return (int value) => value * multiplier;
}

void main() {
  var doubler = makeMultiplier(2);
  var tripler = makeMultiplier(3);
  
  print(doubler(5)); // Output: 10
  print(tripler(5)); // Output: 15
}

6.7 Closures

Dart functions are closures, meaning they can access variables from their outer scope:

void main() {
  var message = 'Hello';
  
  void printMessage() {
    // This function can access the 'message' variable
    print('$message from inner function');
  }
  
  printMessage(); // Output: Hello from inner function
}

This is similar to how closures work in Python.

6.8 Recursive Functions

Dart supports recursive functions (functions that call themselves):

int factorial(int n) {
  if (n <= 1) {
    return 1;
  }
  return n * factorial(n - 1);
}

void main() {
  print(factorial(5)); // Output: 120 (5! = 5*4*3*2*1)
}

6.9 Generator Functions

Dart has special functions called generators that can produce sequences of values:

// Synchronous generator (returns Iterable)
Iterable<int> countUpTo(int n) sync* {
  for (int i = 1; i <= n; i++) {
    yield i;
  }
}

void main() {
  for (var i in countUpTo(5)) {
    print(i); // Output: 1, 2, 3, 4, 5
  }
}

The sync* and yield keywords are used for creating generator functions.

6.10 Functions in Flutter Context

Let’s see how functions are used in a real Flutter widget:

class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;
  
  // Event handler function
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  
  // Helper function with named parameters
  Widget _buildCounterText({required int value, Color color = Colors.black}) {
    return Text(
      'Count: $value',
      style: TextStyle(fontSize: 24, color: color),
    );
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: Center(
        child: _buildCounterText(value: _counter, color: Colors.blue),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter, // Passing function reference
        child: Icon(Icons.add),
      ),
    );
  }
}

In this example, we use: - An event handler function (_incrementCounter) - A helper function with named parameters (_buildCounterText) - Function references (onPressed: _incrementCounter)

6.11 Comparing with Python

Let’s compare Dart functions with Python:

# Python function
def add(a, b):
    return a + b

# Python function with default parameter
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

# Python lambda function
multiply = lambda a, b: a * b
// Dart function
int add(int a, int b) {
  return a + b;
}

// Dart function with default parameter
String greet(String name, {String greeting = "Hello"}) {
  return "$greeting, $name!";
}

// Dart arrow function
int multiply(int a, int b) => a * b;

The main differences are: 1. Dart uses static typing (type declarations for parameters and return values) 2. Dart uses named parameters with curly braces instead of Python’s keyword arguments 3. Dart requires semicolons at the end of statements 4. Dart has arrow functions instead of lambda expressions

6.12 Practical Exercise

Let’s create a small example that uses different types of functions:

void main() {
  // Standard function
  int multiply(int a, int b) {
    return a * b;
  }
  
  // Arrow function with named parameters
  double calculateArea({required double length, required double width}) => length * width;
  
  // Higher-order function
  void applyAndPrint(int value, int Function(int) transformer) {
    var result = transformer(value);
    print('Transformed value: $result');
  }
  
  // Anonymous function
  var square = (int x) => x * x;
  
  print('5 x 3 = ${multiply(5, 3)}');
  print('Area: ${calculateArea(length: 10.5, width: 5.2)}');
  applyAndPrint(4, square);
  applyAndPrint(3, (x) => x * x * x); // Cube function inline
}

This exercise demonstrates:

  • A standard function
  • An arrow function with named parameters
  • A higher-order function
  • Anonymous functions