DART: Functions

Functions are first-class objects in Dart — they can be assigned to variables, passed as arguments, and returned from other functions. Dart's rich function parameter system supports required positional parameters, optional parameters, named parameters, and default values, giving you precise control over every function's API.

Functions are the primary mechanism for organizing and reusing code in Dart. Because functions are objects (instances of the Function class), they can be used anywhere an object can be used — stored in variables, passed to other functions, or returned as results.


Defining Functions

The basic function syntax follows a C-like structure:

dart
// return_type function_name(parameter_type parameter_name) { body }

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

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

double calculateTax(double amount, double rate) {
  return amount * rate;
}

Functions that return nothing use void as the return type. Functions can also use dynamic or inferred return types.


The main() Function

main() is a special function that Dart looks for to start the program. It is the required entry point for every Dart application.

dart
void main() {
  print('Program started');
  printGreeting('Amr');
}


Arrow Functions (Expression Syntax)

For functions that contain a single expression, Dart provides the arrow function shorthand using =>. This is equivalent to writing { return expression; }.

dart
// Full function
int square(int x) {
  return x * x;
}

// Arrow function (equivalent)
int square(int x) => x * x;

// Works with any return type
bool isEven(int n) => n % 2 == 0;
String greet(String name) => 'Hello, $name!';

Arrow functions are commonly used as callbacks and in widget builders where brevity matters.


Anonymous Functions (Lambdas)

An anonymous function (also called a lambda or closure) is a function without a name. It is useful when you need a function as a value — particularly as a callback.

dart
// Named function
void printItem(String item) {
  print(item);
}

List<String> fruits = ['Apple', 'Banana', 'Cherry'];

// Passing a named function
fruits.forEach(printItem);

// Passing an anonymous function
fruits.forEach((item) {
  print(item.toUpperCase());
});

// Arrow shorthand
fruits.forEach((item) => print(item));


Function Parameters

Dart has four categories of parameters, each serving a different purpose.

md
Parameter Types
---------------
Required positional   --> Must be provided, in order
Optional positional   --> May be omitted, matched by position []
Named                 --> May be omitted, matched by name {}
Default               --> Named or optional with a fallback value


Required Positional Parameters

The default parameter type. Arguments must be provided in the declared order.

dart
int multiply(int a, int b) {
  return a * b;
}

print(multiply(3, 4));   // 12
// multiply(3);          // ERROR: Missing required argument


Optional Positional Parameters `[]`

Wrap optional positional parameters in square brackets. If not provided, they default to null (unless a default is specified).

dart
String buildFullName(String firstName, [String? middleName, String? lastName]) {
  final parts = [firstName, if (middleName != null) middleName, if (lastName != null) lastName];
  return parts.join(' ');
}

print(buildFullName('Amr'));                    // Amr
print(buildFullName('Amr', 'Mohamed'));         // Amr Mohamed
print(buildFullName('Amr', 'Mohamed', 'Tarek')); // Amr Mohamed Tarek


Named Parameters `{}`

Named parameters are wrapped in curly braces. They can be passed in any order and are identified by name at the call site.

dart
void greet({required String name, String greeting = 'Hello'}) {
  print('$greeting, $name!');
}

greet(name: 'Alice');                    // Hello, Alice!
greet(name: 'Bob', greeting: 'Hi');      // Hi, Bob!
greet(greeting: 'Hey', name: 'Charlie'); // Hey, Charlie! (order doesn't matter)

Use required to make a named parameter mandatory:

dart
void createUser({required String email, required String password, String? username}) {
  print('Creating: $email');
}

createUser(email: 'test@example.com', password: 'secret123');
// createUser(email: 'test@example.com');  // ERROR: password is required


Default Parameter Values

Any named or optional positional parameter can have a default value, used when the caller does not provide one.

dart
double calculateDiscount(double price, {double discountRate = 0.10}) {
  return price * (1 - discountRate);
}

print(calculateDiscount(100));             // 90.0 (10% default discount)
print(calculateDiscount(100, discountRate: 0.20));  // 80.0 (20% discount)


Return Types and Multiple Returns

Dart functions return a single value. To return multiple values, use a List, Map, or a custom class.

dart
// Return a Map
Map<String, double> divideWithRemainder(int a, int b) {
  return {
    'quotient': (a / b),
    'remainder': (a % b).toDouble(),
  };
}

final result = divideWithRemainder(10, 3);
print(result['quotient']);    // 3.333...
print(result['remainder']);   // 1.0


Methods

A method is a function that belongs to a class (an object). Methods operate on the data stored in the object they belong to.

dart
List<String> fruits = ['Apple', 'Banana', 'Cherry'];

fruits.add('Date');                      // add() is a method on List
fruits.remove('Banana');                 // remove() modifies the list in place
bool hasApple = fruits.contains('Apple');  // contains() returns bool
fruits.sort();                           // sort() modifies in place
String joined = fruits.join(', ');       // join() returns a new String


Closures and Lexical Scope

A closure is a function that captures variables from its surrounding scope, keeping those variables alive even after the enclosing function has returned.

dart
Function makeCounter(int start) {
  int count = start;  // This variable is captured by the inner function

  return () {
    count++;
    return count;
  };
}

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

  print(counter1());  // 1
  print(counter1());  // 2
  print(counter1());  // 3
  print(counter2());  // 11 (independent from counter1)
  print(counter1());  // 4
}

The makeCounter function returns a closure that remembers and modifies the count variable from its outer scope. Each call to makeCounter creates an independent closure with its own count.

dart
// Classic "adder" closure example
Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

var add2 = makeAdder(2);
var add5 = makeAdder(5);

print(add2(3));   // 5
print(add5(3));   // 8
print(add2(10));  // 12


Functions as First-Class Objects

Because functions are objects, they can be stored in variables and passed as arguments.

dart
// Store a function in a variable
int Function(int, int) operation = add;
print(operation(3, 4));  // 7

// Assign a different function to the same variable
operation = multiply;
print(operation(3, 4));  // 12

// Pass a function as a parameter
void applyTwice(void Function(String) fn, String value) {
  fn(value);
  fn(value);
}

applyTwice(print, 'Hello');  // Prints "Hello" twice


Best Practices

  • Keep functions small and focused on a single responsibility
  • Use named parameters for functions with more than two parameters — it improves call-site readability
  • Prefer arrow functions for simple single-expression functions
  • Use closures to encapsulate state rather than relying on global variables
  • Name functions clearly to describe what they do, not how they do it