Callable Classes & the call() Method
What Makes a Class Callable?
In Dart, a class becomes callable -- meaning you can use its instances like functions -- by implementing a special method named call(). When you define a call() method on a class, Dart allows you to invoke instances of that class using function call syntax with parentheses.
This is a powerful pattern that blurs the line between objects and functions, giving you the benefits of both: the stateful behavior of objects and the concise syntax of function calls.
Your First Callable Class
class Greeter {
final String greeting;
Greeter(this.greeting);
// The call() method makes this class callable
String call(String name) {
return '$greeting, $name!';
}
}
void main() {
final hello = Greeter('Hello');
final marhaba = Greeter('Marhaba');
// Call instances like functions!
print(hello('Alice')); // Hello, Alice!
print(marhaba('Ahmad')); // Marhaba, Ahmad!
// You can also call it explicitly
print(hello.call('Bob')); // Hello, Bob!
}
call() method can have any return type, any number of parameters, and can even be generic. There is no interface to implement -- just define a method named call and Dart handles the rest.Closures vs Callable Classes
Dart closures (anonymous functions) can capture variables from their surrounding scope, which gives them state. Callable classes achieve the same thing but with more structure, testability, and reusability.
Comparing Closures and Callable Classes
// Approach 1: Closure with state
Function makeCounter() {
int count = 0;
return () {
count++;
return count;
};
}
// Approach 2: Callable class with state
class Counter {
int _count = 0;
int call() {
_count++;
return _count;
}
// Bonus: callable classes can have extra methods!
void reset() => _count = 0;
int get current => _count;
@override
String toString() => 'Counter(count: $_count)';
}
void main() {
// Closure approach
final closureCounter = makeCounter();
print(closureCounter()); // 1
print(closureCounter()); // 2
// Can't reset or inspect the closure...
// Callable class approach
final counter = Counter();
print(counter()); // 1
print(counter()); // 2
print(counter.current); // 2 -- inspect state
counter.reset(); // reset state
print(counter()); // 1 -- starts over
}
Practical Example: Validators
Callable classes shine as validators -- each validator is an object with configurable rules that you call on input values. This pattern is used extensively in form validation.
Form Validators as Callable Classes
abstract class Validator {
String? call(String? value);
}
class RequiredValidator extends Validator {
final String message;
RequiredValidator([this.message = 'This field is required']);
@override
String? call(String? value) {
if (value == null || value.trim().isEmpty) {
return message;
}
return null; // null means valid
}
}
class MinLengthValidator extends Validator {
final int minLength;
final String? customMessage;
MinLengthValidator(this.minLength, [this.customMessage]);
@override
String? call(String? value) {
if (value != null && value.length < minLength) {
return customMessage ?? 'Must be at least $minLength characters';
}
return null;
}
}
class EmailValidator extends Validator {
@override
String? call(String? value) {
if (value == null || value.isEmpty) return null;
final emailRegex = RegExp(r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+$');
if (!emailRegex.hasMatch(value)) {
return 'Please enter a valid email address';
}
return null;
}
}
class PatternValidator extends Validator {
final RegExp pattern;
final String message;
PatternValidator(this.pattern, this.message);
@override
String? call(String? value) {
if (value != null && !pattern.hasMatch(value)) {
return message;
}
return null;
}
}
// Compose multiple validators
class CompositeValidator extends Validator {
final List<Validator> validators;
CompositeValidator(this.validators);
@override
String? call(String? value) {
for (final validator in validators) {
final error = validator(value);
if (error != null) return error;
}
return null;
}
}
void main() {
// Create validators
final required = RequiredValidator();
final minLength = MinLengthValidator(8);
final email = EmailValidator();
// Use them like functions
print(required('')); // This field is required
print(required('hello')); // null (valid)
print(minLength('abc')); // Must be at least 8 characters
print(email('bad-email')); // Please enter a valid email address
print(email('a@b.com')); // null (valid)
// Compose validators for a password field
final passwordValidator = CompositeValidator([
RequiredValidator('Password is required'),
MinLengthValidator(8, 'Password must be at least 8 characters'),
PatternValidator(
RegExp(r'[A-Z]'),
'Password must contain at least one uppercase letter',
),
PatternValidator(
RegExp(r'[0-9]'),
'Password must contain at least one number',
),
]);
print(passwordValidator('')); // Password is required
print(passwordValidator('abc')); // Password must be at least 8 characters
print(passwordValidator('abcdefgh')); // Password must contain at least one uppercase letter
print(passwordValidator('Abcdefg1')); // null (valid!)
}
CompositeValidator is itself a callable class that contains other callable classes. This is the Composite Pattern -- treating individual objects and collections of objects uniformly. You can nest validators arbitrarily deep.Practical Example: Formatters
Callable classes work beautifully as formatters -- objects that transform data into a display-ready string. Each formatter can have configuration that affects how it formats.
Data Formatters as Callable Classes
class CurrencyFormatter {
final String symbol;
final int decimalPlaces;
final String thousandsSeparator;
CurrencyFormatter({
this.symbol = '\$',
this.decimalPlaces = 2,
this.thousandsSeparator = ',',
});
String call(num amount) {
final fixed = amount.toStringAsFixed(decimalPlaces);
final parts = fixed.split('.');
final intPart = parts[0];
final decPart = parts.length > 1 ? '.${parts[1]}' : '';
// Add thousands separators
final buffer = StringBuffer();
for (int i = 0; i < intPart.length; i++) {
if (i > 0 && (intPart.length - i) % 3 == 0) {
buffer.write(thousandsSeparator);
}
buffer.write(intPart[i]);
}
return '$symbol${buffer.toString()}$decPart';
}
}
class DateFormatter {
final String format;
DateFormatter([this.format = 'yyyy-MM-dd']);
String call(DateTime date) {
return format
.replaceAll('yyyy', date.year.toString().padLeft(4, '0'))
.replaceAll('MM', date.month.toString().padLeft(2, '0'))
.replaceAll('dd', date.day.toString().padLeft(2, '0'))
.replaceAll('HH', date.hour.toString().padLeft(2, '0'))
.replaceAll('mm', date.minute.toString().padLeft(2, '0'));
}
}
class Slugify {
final String separator;
Slugify([this.separator = '-']);
String call(String text) {
return text
.toLowerCase()
.trim()
.replaceAll(RegExp(r'[^\w\s-]'), '')
.replaceAll(RegExp(r'[\s_]+'), separator);
}
}
void main() {
final usd = CurrencyFormatter();
final eur = CurrencyFormatter(symbol: '€', thousandsSeparator: '.');
final formatDate = DateFormatter('dd/MM/yyyy');
final slugify = Slugify();
print(usd(1234567.89)); // $1,234,567.89
print(eur(1234567.89)); // €1.234.567.89
print(formatDate(DateTime(2024, 3, 15))); // 15/03/2024
print(slugify('Hello World! This is Dart')); // hello-world-this-is-dart
}
Practical Example: Middleware & Event Handlers
In application architecture, callable classes are perfect for middleware (processing pipelines) and event handlers -- each step is a configurable object that you chain together.
Middleware Pipeline with Callable Classes
class Request {
final String path;
final Map<String, String> headers;
bool isAuthenticated;
String? userId;
Request(this.path, {Map<String, String>? headers})
: headers = headers ?? {},
isAuthenticated = false;
@override
String toString() => 'Request($path, auth: $isAuthenticated, user: $userId)';
}
class Response {
final int statusCode;
final String body;
const Response(this.statusCode, this.body);
@override
String toString() => 'Response($statusCode: $body)';
}
// Each middleware is a callable class
typedef NextHandler = Response Function(Request);
class LoggingMiddleware {
Response call(Request request, NextHandler next) {
print('[LOG] ${DateTime.now()}: ${request.path}');
final response = next(request);
print('[LOG] Response: ${response.statusCode}');
return response;
}
}
class AuthMiddleware {
final Set<String> validTokens;
AuthMiddleware(this.validTokens);
Response call(Request request, NextHandler next) {
final token = request.headers['Authorization'];
if (token != null && validTokens.contains(token)) {
request.isAuthenticated = true;
request.userId = 'user_${token.hashCode}';
return next(request);
}
return Response(401, 'Unauthorized');
}
}
class RateLimiter {
final int maxRequests;
final Map<String, int> _requestCounts = {};
RateLimiter(this.maxRequests);
Response call(Request request, NextHandler next) {
final ip = request.headers['X-Forwarded-For'] ?? 'unknown';
_requestCounts[ip] = (_requestCounts[ip] ?? 0) + 1;
if (_requestCounts[ip]! > maxRequests) {
return Response(429, 'Too Many Requests');
}
return next(request);
}
}
void main() {
final logger = LoggingMiddleware();
final auth = AuthMiddleware({'token-abc', 'token-xyz'});
final limiter = RateLimiter(100);
// Build a handler pipeline
Response handler(Request req) => Response(200, 'Welcome, ${req.userId}!');
final request = Request(
'/api/profile',
headers: {'Authorization': 'token-abc', 'X-Forwarded-For': '192.168.1.1'},
);
// Chain middleware manually
final response = logger(
request,
(req) => auth(req, (req) => limiter(req, handler)),
);
print(response); // Response(200: Welcome, user_...)
}
Generic Callable Classes
The call() method can use generics, making your callable classes even more flexible.
Generic Transformer and Memoizer
// A generic transformer that maps one type to another
class Transformer<TInput, TOutput> {
final TOutput Function(TInput) _transform;
Transformer(this._transform);
TOutput call(TInput input) => _transform(input);
}
// A memoized callable -- caches results for repeated inputs
class Memoize<TInput, TOutput> {
final TOutput Function(TInput) _fn;
final Map<TInput, TOutput> _cache = {};
Memoize(this._fn);
TOutput call(TInput input) {
return _cache.putIfAbsent(input, () => _fn(input));
}
void clearCache() => _cache.clear();
int get cacheSize => _cache.length;
}
void main() {
// Type-safe transformer
final toUpper = Transformer<String, String>((s) => s.toUpperCase());
final toLength = Transformer<String, int>((s) => s.length);
print(toUpper('hello')); // HELLO
print(toLength('hello')); // 5
// Memoized expensive computation
final fibonacci = Memoize<int, int>((n) {
if (n <= 1) return n;
// Note: this naive version is for demo only
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
final temp = a + b;
a = b;
b = temp;
}
return b;
});
print(fibonacci(10)); // 55
print(fibonacci(10)); // 55 (from cache)
print(fibonacci.cacheSize); // 1
}
Real-World Example: Event System
Here is a complete example showing callable classes as event handlers in a publish-subscribe system -- a pattern found in many Flutter applications.
Event System with Callable Handlers
class Event {
final String type;
final Map<String, dynamic> data;
final DateTime timestamp;
Event(this.type, [Map<String, dynamic>? data])
: data = data ?? {},
timestamp = DateTime.now();
@override
String toString() => 'Event($type, $data)';
}
// Callable event handler
abstract class EventHandler {
void call(Event event);
}
class LogHandler extends EventHandler {
@override
void call(Event event) {
print('[${event.timestamp}] ${event.type}: ${event.data}');
}
}
class ThrottledHandler extends EventHandler {
final EventHandler _inner;
final Duration cooldown;
DateTime? _lastCall;
ThrottledHandler(this._inner, this.cooldown);
@override
void call(Event event) {
final now = DateTime.now();
if (_lastCall == null || now.difference(_lastCall!) >= cooldown) {
_lastCall = now;
_inner(event); // Call the inner handler
}
}
}
class FilteredHandler extends EventHandler {
final EventHandler _inner;
final bool Function(Event) _predicate;
FilteredHandler(this._inner, this._predicate);
@override
void call(Event event) {
if (_predicate(event)) {
_inner(event);
}
}
}
class EventBus {
final Map<String, List<EventHandler>> _handlers = {};
void on(String eventType, EventHandler handler) {
_handlers.putIfAbsent(eventType, () => []).add(handler);
}
void emit(String eventType, [Map<String, dynamic>? data]) {
final event = Event(eventType, data);
final handlers = _handlers[eventType] ?? [];
for (final handler in handlers) {
handler(event); // Call each handler like a function
}
}
}
void main() {
final bus = EventBus();
final logger = LogHandler();
// Register handlers
bus.on('user.login', logger);
bus.on('user.login', FilteredHandler(
LogHandler(),
(e) => e.data['role'] == 'admin',
));
// Emit events
bus.emit('user.login', {'name': 'Alice', 'role': 'user'});
// Only the regular logger fires
bus.emit('user.login', {'name': 'Bob', 'role': 'admin'});
// Both loggers fire for admin login
}
call() method to make instances invocable with function syntax. They are superior to closures when you need state management, extra methods, testability, or interface compliance. Common use cases include validators, formatters, middleware, transformers, and event handlers. Use them when objects need to behave like functions with added capabilities.