Conditionally executes the wrapped function or an alternative based on a predicate.
Control Flow link
Control-flow decorators change whether, how many times, and which implementation runs.
What it is link
Conditionally executes the wrapped function or an alternative based on a predicate.
When to use it link
- Feature flags
- Permission checks
- Input filtering
Async / sync support link
Func<R> | Func1<T, R> | Func2<T1, T2, R> | FuncSync<R> |
|---|
| ✅ | ✅ | ✅ | ❌ |
API reference link
1
2
3
4
5
| // api-reference
Func<R> when({
required bool Function() condition,
Future<R> Function()? otherwise,
})
|
For Func1:
1
2
3
4
5
| // api-reference
Func1<T, R> when({
required bool Function(T arg) condition,
Future<R> Function(T arg)? otherwise,
})
|
For Func2:
1
2
3
4
5
| // api-reference
Func2<T1, T2, R> when({
required bool Function(T1 arg1, T2 arg2) condition,
Future<R> Function(T1 arg1, T2 arg2)? otherwise,
})
|
Throws StateError('Condition not met and no alternative provided') if the condition is false and no otherwise is given.
Examples link
Minimal
1
2
3
4
5
6
7
8
9
| final feature = Func<String>(() async => 'premium')
.when(
condition: () => true,
otherwise: () async => 'basic',
);
void main() async {
print(await feature()); // premium
}
|
Real world
1
2
3
4
5
6
7
8
| final processPayment = Func1<Payment, Result>((payment) async {
return await primaryGateway.charge(payment) as Result;
}).when(
condition: (payment) => featureFlags.isPremium as bool,
otherwise: (payment) async => await secondaryGateway.charge(payment) as Result,
);
await processPayment(Payment());
|
Best practices link
- Keep conditions cheap; they run on every call.
- Provide an
otherwise branch unless failure is intentional.
Common pitfalls link
- Mutable state inside the condition can make behavior unpredictable.
- The wrapped function is not called when the condition is false.
What it is link
Repeats execution a fixed number of times or until a predicate is satisfied.
When to use it link
- Polling
- Retry loops
- Batch processing
Async / sync support link
Func<R> | Func1<T, R> | Func2<T1, T2, R> | FuncSync<R> |
|---|
| ✅ | ✅ | ✅ | ❌ |
API reference link
1
2
3
4
5
6
7
| // api-reference
Func<R> repeat({
int? times,
Duration? interval,
bool Function(R result)? until,
void Function(int iteration, R result)? onIteration,
})
|
times — max iterations; null means infinite.interval — delay between iterations.until — stops when predicate returns true.onIteration — called after each iteration.
Examples link
Minimal
1
2
3
4
5
6
7
8
9
10
| final poll = Func<String>(() async => 'ready')
.repeat(
times: 5,
interval: Duration(milliseconds: 100),
until: (result) => result == 'ready',
);
void main() async {
print(await poll()); // ready
}
|
Real world
1
2
3
4
5
6
7
8
9
10
| final waitForBuild = Func1<String, Status>((buildId) async {
return await ciApi.status(buildId) as Status;
}).repeat(
times: 60,
interval: Duration(seconds: 5),
until: (status) => status.ready,
onIteration: (i, status) => logger.info('Poll $i: $status'),
);
await waitForBuild('build-123');
|
Best practices link
- Always bound loops with
times or until. - Use
retry instead of repeat for transient-error retries with backoff.
Common pitfalls link
- Infinite loops are possible if
times and until are both null. - Errors are not caught; use with
retry or defaultValue if needed.
switch (SwitchExtension) link
What it is link
Routes execution to one of several function implementations based on a selector.
When to use it link
- Strategy pattern
- Polymorphic dispatch
- Request routing
Async / sync support link
Func<R> | Func1<T, R> | Func2<T1, T2, R> | FuncSync<R> |
|---|
| ❌ | ✅ | ✅ | ❌ |
API reference link
1
2
3
4
5
6
| // api-reference
SwitchExtension1<T, R>({
required Object? Function(T arg) selector,
required Map<Object?, Func1<T, R>> cases,
Func1<T, R>? defaultCase,
})
|
For Func2:
1
2
3
4
5
6
| // api-reference
SwitchExtension2<T1, T2, R>({
required Object? Function(T1 arg1, T2 arg2) selector,
required Map<Object?, Func2<T1, T2, R>> cases,
Func2<T1, T2, R>? defaultCase,
})
|
Throws SwitchException if no case matches and no defaultCase is provided.
Examples link
Minimal
1
2
3
4
5
6
7
8
9
10
11
12
| final handle = SwitchExtension1<String, String>(
selector: (type) => type,
cases: {
'a': Func1((_) async => 'A'),
'b': Func1((_) async => 'B'),
},
defaultCase: Func1((_) async => '?'),
);
void main() async {
print(await handle('a')); // A
}
|
Real world
1
2
3
4
5
6
7
8
9
10
| final processPayment = SwitchExtension1<String, Result>(
selector: (method) => method,
cases: {
'card': Func1<String, Result>((method) async => cardProcessor.charge(method) as Result),
'bank': Func1<String, Result>((method) async => bankProcessor.transfer(method) as Result),
},
defaultCase: Func1<String, Result>((method) async => Result()),
);
await processPayment('card');
|
Best practices link
- Provide a
defaultCase to avoid exceptions for unknown selectors. - Keep case handlers small and focused.
Common pitfalls link
SwitchExtension is a standalone class used directly, not chained via a decorator method.- Matching uses equality on selector result; ensure keys have stable equality.