Complete API documentation for flutter_debounce_throttle v2.4.4.
- Basic Limiters
- Async Limiters
- Advanced Limiters
- Flutter Widgets
- Flutter Hooks
- Extensions
- State Management Mixin
Execute immediately, block subsequent calls for duration.
final throttler = Throttler(duration: Duration(milliseconds: 500));
// Execute immediately, block for 500ms
throttler.call(() => print('Executed!'));
throttler.call(() => print('Blocked!'));
// Wrap as VoidCallback
button.onPressed = throttler.wrap(() => submit());
// Cleanup
throttler.dispose();Delay execution until pause in calls.
final debouncer = Debouncer(duration: Duration(milliseconds: 300));
// Delays execution until 300ms pause
debouncer.call(() => search(query));
debouncer.call(() => search(query)); // Resets timer
// Only last call executes after 300ms
// Force immediate execution
debouncer.flush();
// Cancel pending
debouncer.cancel();
// Cleanup
debouncer.dispose();// Default: trailing edge only (standard debounce)
final debouncer = Debouncer(
duration: Duration(milliseconds: 300),
trailing: true, // Execute after pause (default)
);
// Leading edge: execute immediately on first call
final buttonDebouncer = Debouncer(
duration: Duration(milliseconds: 300),
leading: true, // Execute immediately
trailing: false, // Don't execute after pause
);
// Both edges (like lodash _.debounce)
final hybridDebouncer = Debouncer(
duration: Duration(milliseconds: 300),
leading: true, // Execute immediately on first call
trailing: true, // Also execute after pause if new calls came
);| Mode | First Call | During Debounce | After Pause |
|---|---|---|---|
trailing: true (default) |
Waits | Resets timer | Executes |
leading: true |
Executes | Blocked | - |
leading + trailing |
Executes | Resets timer | Executes (if new calls) |
final debouncer = AsyncDebouncer(duration: Duration(milliseconds: 300));
// Callable class pattern
final result = await debouncer(() async {
return await api.search(query);
});
// result is null if cancelled
if (result != null) {
updateUI(result);
}Use callWithResult() when your async operation can return null:
// Problem: call() returns T? - can't distinguish "cancelled" from "result is null"
final user = await debouncer(() async => await api.findUser(id));
if (user == null) {
// Cancelled? Or user not found?
}
// Solution: callWithResult() returns DebounceResult<T>
final result = await debouncer.callWithResult(() async => await api.findUser(id));
if (result.isCancelled) {
return; // Cancelled by newer call
}
// result.value may be null (user not found), but we know it wasn't cancelled
showUser(result.value);DebounceResult properties:
isCancelled- true if operation was cancelledisSuccess- true if operation completed (not cancelled)value- the result value (may be null even if successful)
final throttler = AsyncThrottler(maxDuration: Duration(seconds: 5));
// Lock until async operation completes (or timeout)
final result = await throttler.call(() async {
return await api.submit(data);
});
// Check if currently locked
if (throttler.isLocked) {
showLoading();
}Warning: Always set
maxDurationto prevent UI lockup if async operation hangs.
Handle concurrent async operations with 4 modes:
final throttler = ConcurrentAsyncThrottler(
mode: ConcurrencyMode.replace,
maxDuration: Duration(seconds: 10),
);
throttler.call(() async => await fetchData());| Mode | Behavior |
|---|---|
drop |
Ignore new calls while busy |
enqueue |
Queue and execute in order |
replace |
Cancel current, start new |
keepLatest |
Keep only latest pending |
final chatSender = ConcurrentAsyncThrottler(
mode: ConcurrencyMode.enqueue,
maxDuration: Duration(seconds: 30),
maxQueueSize: 10,
queueOverflowStrategy: QueueOverflowStrategy.dropOldest,
);
// QueueOverflowStrategy options:
// - dropNewest: Reject new calls when queue is full (default)
// - dropOldest: Remove oldest queued call to make roomOptimized for high-frequency events (scroll, mouse move):
final throttler = HighFrequencyThrottler(
duration: Duration(milliseconds: 16), // 60fps
);
// Uses DateTime comparison instead of Timer
scrollController.addListener(() {
throttler.call(() => updateUI());
});Batch multiple operations into single execution:
final batcher = BatchThrottler(
duration: Duration(milliseconds: 100),
onBatchExecute: (actions) {
for (final action in actions) {
action();
}
},
);
batcher(() => save('item1'));
batcher(() => save('item2'));
batcher(() => save('item3'));
// After 100ms: executes all 3 actions in batchfinal batcher = BatchThrottler(
duration: Duration(milliseconds: 500),
maxBatchSize: 5,
overflowStrategy: BatchOverflowStrategy.dropOldest,
onBatchExecute: (actions) { /* ... */ },
);
// BatchOverflowStrategy options:
// - dropOldest: Remove oldest item when full
// - dropNewest: Reject new item when full
// - flushAndAdd: Flush immediately, then add new itemToken Bucket algorithm for burst-capable rate limiting:
final limiter = RateLimiter(
maxTokens: 10, // Burst capacity
refillRate: 2, // 2 tokens per interval
refillInterval: Duration(seconds: 1),
);
// Check before calling
if (limiter.tryAcquire()) {
await api.call();
} else {
showRateLimitError();
}
// Or use with callback
final executed = limiter.call(() => api.submit());
// Async version
final result = await limiter.callAsync(() async => await api.getData());
// Check status
print('Available: ${limiter.availableTokens}');
print('Time until next: ${limiter.timeUntilNextToken}');ThrottledBuilder(
duration: Duration(milliseconds: 500),
builder: (context, throttle) => ElevatedButton(
onPressed: throttle(() => submit()),
child: Text('Submit'),
),
)ThrottledInkWell(
duration: Duration(milliseconds: 500),
onTap: () => navigate(),
child: ListTile(title: Text('Go to Details')),
)DebouncedBuilder(
duration: Duration(milliseconds: 300),
builder: (context, debounce) => TextField(
onChanged: (text) => debounce(() => validate(text))?.call(),
),
)DebouncedQueryBuilder<List<User>>(
duration: Duration(milliseconds: 300),
onQuery: (query) async => await searchUsers(query),
onResult: (users) => setState(() => _users = users),
onError: (e) => showError(e),
builder: (context, search, isLoading) => Column(
children: [
TextField(onChanged: search),
if (isLoading) LinearProgressIndicator(),
],
),
)ConcurrentAsyncThrottledBuilder(
mode: ConcurrencyMode.replace,
builder: (context, throttle, isLoading) => ElevatedButton(
onPressed: isLoading ? null : () => throttle(() async {
await uploadFile();
}),
child: isLoading ? CircularProgressIndicator() : Text('Upload'),
),
)// Auto-cancel on dispose
StreamSafeListener<int>(
stream: counterStream,
onData: (value) => updateCounter(value),
child: CounterDisplay(),
)
// Debounce stream events
StreamDebounceListener<String>(
stream: searchQueryStream,
duration: Duration(milliseconds: 300),
onData: (query) => performSearch(query),
child: SearchResults(),
)
// Throttle stream events
StreamThrottleListener<Offset>(
stream: mouseMoveStream,
duration: Duration(milliseconds: 16),
onData: (position) => updateCursor(position),
child: Canvas(),
)// Debounced text controller
final controller = DebouncedTextController(
duration: Duration(milliseconds: 300),
onChanged: (text) => validate(text),
);
// Async search controller
final controller = AsyncDebouncedTextController<List<User>>(
duration: Duration(milliseconds: 300),
onSearch: (query) async => await api.search(query),
onResults: (users) => setState(() => _users = users),
onLoading: (loading) => setState(() => _isLoading = loading),
);import 'package:flutter_debounce_throttle_hooks/flutter_debounce_throttle_hooks.dart';
class MyWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final debouncer = useDebouncer(duration: Duration(milliseconds: 300));
final throttler = useThrottler(duration: Duration(milliseconds: 500));
final debouncedSearch = useDebouncedCallback<String>(
(text) => search(text),
duration: Duration(milliseconds: 300),
);
final throttledSubmit = useThrottledCallback(
() => submit(),
duration: Duration(milliseconds: 500),
);
final debouncedText = useDebouncedValue(
searchText.value,
duration: Duration(milliseconds: 300),
);
return Column(
children: [
TextField(onChanged: debouncedSearch),
ElevatedButton(onPressed: throttledSubmit, child: Text('Submit')),
],
);
}
}| Hook | Returns | Description |
|---|---|---|
useDebouncer |
Debouncer |
Debouncer instance |
useThrottler |
Throttler |
Throttler instance |
useAsyncDebouncer |
AsyncDebouncer |
Async debouncer |
useAsyncThrottler |
AsyncThrottler |
Async throttler |
useDebouncedCallback<T> |
void Function(T) |
Debounced callback |
useThrottledCallback |
VoidCallback |
Throttled callback |
useDebouncedValue<T> |
T |
Debounced value |
useThrottledValue<T> |
T |
Throttled value |
300.ms // Duration(milliseconds: 300)
2.seconds // Duration(seconds: 2)
5.minutes // Duration(minutes: 5)
1.hours // Duration(hours: 1)final search = () => api.search();
final debouncedSearch = search.debounced(300.ms);
final throttledSearch = search.throttled(500.ms);Works with Provider, GetX, Bloc, Riverpod, MobX, etc.
class MyController with ChangeNotifier, EventLimiterMixin {
void onSearchChanged(String text) {
debounce('search', () async {
users = await api.search(text);
notifyListeners();
});
}
void onButtonPressed() {
throttle('submit', () async {
await api.submit();
});
}
@override
void dispose() {
cancelAll(); // Clean up all limiters
super.dispose();
}
}mixin EventLimiterMixin {
void debounce(String id, VoidCallback action, {Duration? duration});
void throttle(String id, VoidCallback action, {Duration? duration});
Future<T?> debounceAsync<T>(String id, Future<T> Function() action);
Future<T?> throttleAsync<T>(String id, Future<T> Function() action);
void cancel(String id);
void cancelAll();
bool isLimiterActive(String id);
int get activeLimitersCount;
}Important: Mixin does NOT auto-dispose. Call
cancelAll()in your controller'sdispose()method.
Auto-cleanup is enabled by default to prevent memory leaks:
void main() {
// Default configuration (v2.3.0+):
// - limiterAutoCleanupTTL: Duration(minutes: 10)
// - limiterAutoCleanupThreshold: 100
// Optional: Customize settings
DebounceThrottleConfig.init(
limiterAutoCleanupTTL: Duration(minutes: 5), // Faster cleanup
limiterAutoCleanupThreshold: 50, // Lower threshold
);
runApp(MyApp());
}To disable auto-cleanup (not recommended):
DebounceThrottleConfig.init(
limiterAutoCleanupTTL: null, // ⚠️ Requires manual cleanup!
);void main() {
DebounceThrottleConfig.init(
enableDebugLog: kDebugMode,
logLevel: LogLevel.warning,
logHandler: (level, message, name, timestamp) {
print('[$level] $message');
},
);
runApp(MyApp());
}final debouncer = Debouncer(
duration: Duration(milliseconds: 300),
debugMode: true,
name: 'SearchDebouncer',
);
// Output:
// [SearchDebouncer] call() - scheduling
// [SearchDebouncer] _execute() - executing callback