Custom Functions
Overview
Custom functions are user-defined functions that extend the expression language with your own logic. There are two ways to add custom functions, each with different scope and lifetime. Beyond functions, resolvers extend method and property access to your own types.
All custom functions can only accept positional arguments — named parameters are not supported.
Registered Functions
Use registerFunction to add functions that are globally available in every EL
expression, just like built-in functions. Register them once at startup and they're
accessible everywhere without passing them through Dependencies.
import 'package:xwidget_el/xwidget_el.dart';
void main() {
// Register global custom functions
registerFunction("formatCurrency", formatCurrency);
registerFunction("pluralize", pluralize);
}
String formatCurrency(dynamic value) {
final amount = (value as num).toStringAsFixed(2);
return '\$$amount';
}
String pluralize(dynamic count, String singular, String plural) {
return (count as num) == 1 ? singular : plural;
}
<Text data="${formatCurrency(price)}"/>
<!-- 19.99 -> "$19.99" -->
<Text data="${pluralize(itemCount, 'item', 'items')}"/>
<!-- 1 -> "item", 5 -> "items" -->
Resolution Order
When an EL expression calls a function, it is resolved in this order:
- Built-in functions — the predefined set (e.g.
abs,length,isEmpty) - Registered functions — added via
registerFunction - Dependency functions — stored in the current
Dependenciesinstance
Built-in functions cannot be overridden by registered functions. Registered functions take priority over dependency functions of the same name.
Dependency Functions
Add functions to a Dependencies instance for scoped access. These are available
only where that Dependencies instance is in scope, making them ideal for
controller-specific logic.
final dependencies = Dependencies();
dependencies.setValue("greet", (String name) => 'Hello, $name!');
Dependency functions are commonly used in controllers to expose methods to fragments:
class ProductController extends Controller {
@override
void bindDependencies() {
dependencies.setValue("formatSku", formatSku);
}
String formatSku(dynamic sku) => 'SKU-${sku.toString().padLeft(6, '0')}';
}
When to Use Which
| Registered | Dependency | |
|---|---|---|
| Scope | Global — available everywhere | Scoped — only where Dependencies is accessible |
| Lifetime | App lifetime | Tied to the Dependencies instance |
| Registration | Once at startup | Each time Dependencies is created |
| Use case | Utility functions, formatters, shared logic | Controller-specific methods, context-dependent logic |
Instance Method Resolvers
Use registerMethodResolver to make methods on your own types callable from
expressions. Resolvers run after the built-in
instance functions fall through, in registration order;
the first resolver that returns a non-null Function wins. Built-in instance
functions cannot be overridden. Return null to defer to the next resolver.
import 'package:xwidget_el/xwidget_el.dart';
void main() {
registerMethodResolver((name, target) {
if (target is Money) {
switch (name) {
case "format": return target.format;
}
}
return null;
});
}
Property Resolvers
Use registerPropertyResolver to give your own types
properties. Return a PropertyResolution wrapping the value, or
null to defer — the wrapper is what distinguishes "resolved to null" from "not
resolved." Resolvers run after the built-in core properties, in registration order;
built-in properties cannot be overridden.