Function.apply in Dart
last modified May 25, 2025
This tutorial explores Function.apply
in Dart, a powerful feature
for dynamic function invocation. Function.apply
allows calling
functions and constructors with arguments provided as a list, enabling
metaprogramming patterns and flexible API designs.
Function.apply Overview
Function.apply
is a static method that invokes a function with
a list of positional arguments and an optional map of named arguments. It
provides a way to call functions dynamically, which is particularly useful
for implementing plugins, command dispatchers, or other patterns where the
exact function to call isn't known at compile time.
The method signature is:
static apply(Function function, List positionalArguments, [Map<Symbol, dynamic> namedArguments])
It returns whatever the invoked function returns, and can throw whatever exceptions the invoked function throws.
Parameter | Type | Description |
---|---|---|
function |
Function | The function to invoke |
positionalArguments |
List | List of positional arguments |
namedArguments |
Map<Symbol, dynamic> | Optional map of named arguments |
Function.apply
is part of Dart's limited reflection capabilities.
While Dart doesn't have full runtime reflection like some other languages,
Function.apply
provides just enough functionality for many
dynamic invocation scenarios without compromising tree-shaking and
optimizations.
Basic Function.apply Usage
This example demonstrates basic usage of Function.apply
with
regular functions, constructors, and method calls. We'll use the provided
Point
class and functions to show different invocation patterns.
class Point { final int x, y, z; Point(this.x, this.y, this.z); @override String toString() => 'Point($x, $y, $z)'; } int sum(int a, int b, int c) => a + b + c; void printNames(String first, String second, String third) { print('Names: $first, $second, $third'); } void main() { var numbers = [1, 2, 3]; // Using Function.apply for sum function var result = Function.apply(sum, numbers); print('Sum: $result'); // Output: Sum: 6 // Using Function.apply for constructor invocation var coords = [4, 5, 6]; var point = Function.apply(Point.new, coords); print('Point: $point'); // Output: Point(4, 5, 6) // Using Function.apply for printNames function List<String>? names = ['Alice', 'Bob', 'Charlie']; if (names != null && names.length == 3) { Function.apply(printNames, names); } else { print('Names list is invalid.'); } // Using Function.apply with dynamically retrieved values var userData = getUserCoordinates(); var userPoint = Function.apply(Point.new, userData); print('User point: $userPoint'); // Output: User point: Point(10, 20, 30) } List<int> getUserCoordinates() => [10, 20, 30];
The example shows four key uses of Function.apply
: calling a
regular function (sum
), invoking a constructor
(Point.new
), calling a void function (printNames
),
and creating objects with dynamically retrieved values. Each demonstrates
how argument lists can be constructed at runtime.
Note the constructor invocation using Point.new
- this syntax
references the constructor as a callable function object. The argument count
must match the function's parameter count, or an exception will be thrown.
$ dart run basic_apply.dart Sum: 6 Point: Point(4, 5, 6) Names: Alice, Bob, Charlie User point: Point(10, 20, 30)
Named Arguments with Function.apply
Function.apply
also supports named arguments through a map of
Symbol
keys to values. This example demonstrates how to use
named arguments dynamically.
class Settings { final String theme; final bool darkMode; final int fontSize; Settings({required this.theme, this.darkMode = false, this.fontSize = 14}); @override String toString() => 'Settings(theme: $theme, darkMode: $darkMode, fontSize: $fontSize)'; } void configureApp({String? theme, bool? notifications, int? timeout}) { print('Configuring with: theme=$theme, notifications=$notifications, timeout=$timeout'); } void main() { // Using named arguments with a constructor var settingsArgs = { #theme: 'dark', #darkMode: true, #fontSize: 16, }; var settings = Function.apply(Settings.new, [], settingsArgs); print(settings); // Using named arguments with a function var configArgs = { #theme: 'light', #notifications: true, }; Function.apply(configureApp, [], configArgs); // Partial arguments with defaults var minimalArgs = {#theme: 'blue'}; var minimalSettings = Function.apply(Settings.new, [], minimalArgs); print(minimalSettings); }
Named arguments are passed using a map where keys are Symbol
s
(created with the #
syntax) and values are the argument values.
The example shows both constructor and function invocation with named
arguments, including cases where some optional parameters are omitted.
When using named parameters, the argument map keys must exactly match the function's parameter names (as Symbols). The positional arguments list should be empty when only using named arguments.
$ dart run named_arguments.dart Settings(theme: dark, darkMode: true, fontSize: 16) Configuring with: theme=light, notifications=true, timeout=null Settings(theme: blue, darkMode: false, fontSize: 14)
Error Handling and Validation
When using Function.apply
, it's important to handle potential
errors. This example demonstrates common error scenarios and how to validate
arguments before invocation.
int multiply(int a, int b) => a * b; class Product { final String id; final String name; Product(this.id, this.name); @override String toString() => 'Product(id: $id, name: $name)'; } void main() { // Argument count mismatch try { Function.apply(multiply, [4]); // Missing argument } catch (e) { print('Error: $e'); // Invalid argument count } // Type mismatch try { Function.apply(multiply, ['a', 'b']); // Wrong types } catch (e) { print('Error: $e'); // Type error } // Safe invocation with validation var productData = getProductData(); if (productData is List && productData.length == 2) { var product = Function.apply(Product.new, productData); print(product); } else { print('Invalid product data'); } // Handling null values List<String>? names = getNames(); if (names != null && names.length >= 2) { Function.apply(printNames, names.take(3).toList()); } else { print('No valid names available'); } } void printNames(String name1, String name2, [String? name3]) { print('Names: $name1, $name2${name3 != null ? ', $name3' : ''}'); } dynamic getProductData() => ['123', 'Dart Book']; List<String>? getNames() => ['Alice', 'Bob'];
The example shows several failure cases: argument count mismatch, type
mismatch, and null safety issues. It then demonstrates defensive programming
patterns to validate arguments before calling Function.apply
.
Always validate argument lists when they come from external sources. The
is
operator and null checks help ensure type safety before
invocation. Consider wrapping Function.apply
in try-catch
blocks when dealing with untrusted input.
$ dart run error_handling.dart Error: NoSuchMethodError: Closure call with mismatched arguments: function 'multiply' Receiver: Closure: (int, int) => int from Function 'multiply': static. Tried calling: multiply(4) Found: multiply(int, int) => int Error: type 'String' is not a subtype of type 'int' of 'a' Product(id: 123, name: Dart Book) Names: Alice, Bob
Dynamic Function Selection
Function.apply
shines when combined with dynamic function
selection. This example shows how to implement a simple command pattern
where the function to call is determined at runtime.
class Calculator { static double add(List<double> args) => args.reduce((a, b) => a + b); static double multiply(List<double> args) => args.reduce((a, b) => a * b); static double average(List<double> args) => add(args) / args.length; } void main() { var operations = { 'sum': Calculator.add, 'product': Calculator.multiply, 'mean': Calculator.average, }; var command = 'sum'; // Could come from user input var numbers = [1.5, 2.5, 3.5]; if (operations.containsKey(command)) { var result = Function.apply(operations[command]!, [numbers]); print('Result of $command: $result'); } else { print('Unknown operation: $command'); } // More complex example with validation var userInput = getUserInput(); processCommand(userInput['command'], userInput['args']); } void processCommand(String? command, List<double>? args) { var operations = { 'sum': Calculator.add, 'product': Calculator.multiply, 'mean': Calculator.average, }; if (command == null || !operations.containsKey(command)) { print('Invalid command'); return; } if (args == null || args.isEmpty) { print('No arguments provided'); return; } try { var result = Function.apply(operations[command]!, [args]); print('$command result: $result'); } catch (e) { print('Error processing command: $e'); } } Map<String, dynamic> getUserInput() => { 'command': 'product', 'args': [2.0, 3.0, 4.0] };
This pattern is useful for command processors, calculators, or plugin systems where the operation to perform isn't known until runtime. The example shows both simple and more robust implementations with input validation.
The function map (operations
) serves as a registry of available
operations. The Function.apply
call dynamically selects and
executes the appropriate function based on the command name. This approach
makes it easy to add new operations without modifying the command processing
logic.
$ dart run dynamic_selection.dart Result of sum: 7.5 product result: 24.0
Practical Use Case: JSON Processing
This example demonstrates a practical use of Function.apply
for
dynamic object creation from JSON data. We'll create objects based on type
information in the JSON.
class User { final String id; final String name; final int age; User(this.id, this.name, this.age); @override String toString() => 'User(id: $id, name: $name, age: $age)'; } class Product { final String sku; final String description; final double price; Product(this.sku, this.description, this.price); @override String toString() => 'Product(sku: $sku, description: $description, price: $price)'; } void main() { var jsonData = { 'type': 'user', 'data': ['u123', 'Alice', 30] }; var object = createFromJson(jsonData); print(object); jsonData = { 'type': 'product', 'data': ['p456', 'Dart Programming Book', 39.99] }; object = createFromJson(jsonData); print(object); } dynamic createFromJson(Map<String, dynamic> json) { var constructors = { 'user': User.new, 'product': Product.new, }; var type = json['type']; var data = json['data']; if (type == null || !constructors.containsKey(type)) { throw ArgumentError('Unknown type: $type'); } if (data is! List) { throw ArgumentError('Data must be a list'); } try { return Function.apply(constructors[type]!, data); } catch (e) { throw ArgumentError('Failed to create $type: $e'); } }
This pattern is useful when deserializing JSON data where the object type is
specified in the data itself. The createFromJson
function uses a
registry of constructors and Function.apply
to create the
appropriate object based on the JSON contents.
The example shows robust error handling for invalid type specifications or data formats. This approach can be extended to support more complex object graphs or additional validation rules.
$ dart run json_processing.dart User(id: u123, name: Alice, age: 30) Product(sku: p456, description: Dart Programming Book, price: 39.99)
Source
Dart Function.apply API
Dart Functions Documentation
Dart Constructors Documentation
Function.apply
is a powerful tool for dynamic invocation in Dart.
While it should be used judiciously (as it bypasses some static type checking),
it enables flexible patterns like plugin systems, command processors, and
dynamic object creation. When combined with proper validation and error
handling, it can significantly enhance the flexibility of your Dart
applications.
Author
List all Dart tutorials.