Production-ready throttle and debounce for Flutter apps.
Stop wrestling with Timer boilerplate, race conditions, and setState crashes. Handle button spam, search debouncing, and async operations with 3 lines of code instead of 15+.
Timer? _timer;
bool _loading = false;
void onSearch(String text) {
_timer?.cancel();
_timer = Timer(Duration(milliseconds: 300), () async {
setState(() => _loading = true);
try {
final result = await api.search(text);
if (!mounted) return; // Easy to forget!
setState(() => _result = result);
} finally {
if (mounted) setState(() => _loading = false);
}
});
}
@override
void dispose() {
_timer?.cancel(); // Easy to forget!
super.dispose();
}AsyncDebouncedTextController(
onChanged: (text) async => await api.search(text),
onSuccess: (result) => setState(() => _result = result),
onLoadingChanged: (loading) => setState(() => _loading = loading),
)Result: 80% less code. Auto-dispose. Auto mounted checks. Auto loading state.
Built for production use:
- โ 160/160 pub points - Perfect score, actively maintained
- โ 128 comprehensive tests - Edge cases covered, battle-tested
- โ Zero dependencies - No bloat, no conflicts
Saves development time:
- โ 3 lines vs 15+ - Eliminates boilerplate
- โ Auto-safety - No setState crashes, no memory leaks
- โ Works with any widget - Material, Cupertino, custom widgets
Unique features:
- โ
Built-in loading state - No manual
bool isLoading = falseneeded - โ Race condition prevention - Auto-cancels stale API calls
- โ Universal builders - Not locked into specific widgets
See detailed comparison with alternatives โ
ThrottledInkWell(
onTap: () => submitOrder(), // Only executes once per 500ms
child: Text("Submit Order"),
)AsyncDebouncedTextController(
duration: Duration(milliseconds: 300),
onChanged: (text) async => await api.search(text),
onSuccess: (products) => setState(() => _products = products),
onLoadingChanged: (isLoading) => setState(() => _loading = isLoading),
)AsyncThrottledCallbackBuilder(
onPressed: () async => await uploadFile(),
builder: (context, callback, isLoading) {
return ElevatedButton(
onPressed: isLoading ? null : callback,
child: isLoading ? CircularProgressIndicator() : Text("Upload"),
);
},
)Control how multiple async operations are handled with 4 powerful modes:
// Chat App: Queue messages and send in order
ConcurrentAsyncThrottledBuilder(
mode: ConcurrencyMode.enqueue,
onPressed: () async => await api.sendMessage(text),
builder: (context, callback, isLoading, pendingCount) {
return ElevatedButton(
onPressed: callback,
child: Text(pendingCount > 0 ? 'Sending ($pendingCount)...' : 'Send'),
);
},
)
// Search: Cancel old queries, only run latest
ConcurrentAsyncThrottledBuilder(
mode: ConcurrencyMode.replace,
onPressed: () async => await api.search(query),
builder: (context, callback, isLoading, _) {
return SearchBar(
onChanged: (q) { query = q; callback?.call(); },
trailing: isLoading ? CircularProgressIndicator() : null,
);
},
)
// Auto-save: Save current + latest only
ConcurrentAsyncThrottledBuilder(
mode: ConcurrencyMode.keepLatest,
onPressed: () async => await api.saveDraft(content),
builder: (context, callback, isLoading, _) {
return TextField(
onChanged: (text) { content = text; callback?.call(); },
decoration: InputDecoration(
suffixIcon: isLoading ? CircularProgressIndicator() : Icon(Icons.check),
),
);
},
)4 Concurrency Modes:
- ๐ด Drop (default): Ignore new calls while busy - perfect for preventing double-clicks
- ๐ค Enqueue: Queue all calls and execute sequentially - perfect for chat apps
- ๐ Replace: Cancel current and start new - perfect for search queries
- ๐พ Keep Latest: Execute current + latest only - perfect for auto-save
The power of flexibility: Works with any Flutter widget.
ThrottledBuilder(
duration: Duration(seconds: 1),
builder: (context, throttle) {
return FloatingActionButton(
onPressed: throttle(() => saveData()),
child: Icon(Icons.save),
);
},
)Use with:
- โ
Material Design (
ElevatedButton,FloatingActionButton,InkWell) - โ
Cupertino (
CupertinoButton,CupertinoTextField) - โ Custom widgets from any package
- โ Third-party UI libraries
Unlike other libraries that lock you into specific widgets, flutter_event_limiter adapts to your UI framework.
Fires immediately, then blocks for duration.
User clicks: โผ โผ โผโผโผ โผ
Executes: โ X X X โ
|<-500ms->| |<-500ms->|
Use for: Button clicks, refresh actions, preventing spam
Waits for pause in events, then fires.
User types: a b c d ... (pause) ... e f g
Executes: โ โ
|<--300ms wait-->| |<--300ms wait-->|
Use for: Search input, auto-save, slider changes
Waits for pause and cancels previous async operations.
User types: a b c (API starts) ... d
API calls: X X โผ (running...) X (cancelled)
Result used: โ (only 'd')
Use for: Search APIs, autocomplete, async validation
Learn more about timing strategies โ
| Widget | Use Case |
|---|---|
ThrottledInkWell |
Material buttons with ripple |
ThrottledBuilder |
Universal - Any widget |
AsyncThrottledCallbackBuilder |
Async with auto loading state |
Throttler |
Direct class (advanced) |
| Widget | Use Case |
|---|---|
DebouncedTextController |
Basic text input |
AsyncDebouncedTextController |
Search API with loading |
DebouncedBuilder |
Universal - Any widget |
Debouncer |
Direct class (advanced) |
| Widget | Use Case |
|---|---|
HighFrequencyThrottler |
Scroll, mouse, resize (60fps) |
View full API documentation โ
flutter pub add flutter_event_limiterOr add to pubspec.yaml:
dependencies:
flutter_event_limiter: ^1.1.2Then import:
import 'package:flutter_event_limiter/flutter_event_limiter.dart';- Quick Start Guide
- Throttle vs Debounce Explained
- FAQ - Common questions answered
- E-Commerce: Prevent Double Checkout
- Search with Race Condition Prevention
- Form Submission with Loading State
- Chat App: Prevent Message Spam
- From easy_debounce - Stop managing IDs manually
- From flutter_smart_debouncer - Unlock from fixed widgets
- From rxdart - Simpler API for UI events
- Detailed Comparison with Alternatives
- Roadmap - Upcoming features
- API Reference
Core Capabilities:
- โฑ๏ธ Throttle - Execute immediately, block duplicates
- โณ Debounce - Wait for pause, then execute
- ๐ Async Support - Built-in for async operations
- ๐ High Frequency - Optimized for scroll/mouse events (60fps)
- ๐ญ Combo - ThrottleDebouncer (leading + trailing)
Safety & Reliability:
- ๐ก๏ธ Auto Dispose - Zero memory leaks
- โ Auto Mounted Checks - No setState crashes
- ๐ Race Condition Prevention - Auto-cancel stale calls
- ๐ฆ Batch Execution - Group multiple operations
Developer Experience:
- ๐จ Universal Builders - Works with any widget
- ๐ Built-in Loading State - No manual flags
- ๐ Debug Mode - Log throttle/debounce events
- ๐ Performance Metrics - Track execution time
- โ๏ธ Conditional Execution - Enable/disable dynamically
Works seamlessly with Flutter's test framework:
testWidgets('throttle blocks rapid clicks', (tester) async {
int clickCount = 0;
await tester.pumpWidget(
MaterialApp(
home: ThrottledInkWell(
duration: Duration(milliseconds: 500),
onTap: () => clickCount++,
child: Text('Tap'),
),
),
);
await tester.tap(find.text('Tap'));
expect(clickCount, 1);
await tester.tap(find.text('Tap')); // Blocked
expect(clickCount, 1);
await tester.pumpAndSettle(Duration(milliseconds: 500));
await tester.tap(find.text('Tap')); // Works again
expect(clickCount, 2);
});See more testing examples in FAQ โ
Throttler(
debugMode: true,
name: 'submit-button',
onMetrics: (duration, executed) {
print('Throttle took: $duration, executed: $executed');
},
)ThrottledBuilder(
enabled: !isVipUser, // VIP users skip throttle
builder: (context, throttle) => ElevatedButton(...),
)final throttler = Throttler();
throttler.callWithDuration(
() => criticalAction(),
duration: Duration(seconds: 2), // Override default
);final throttler = Throttler();
throttler.call(() => action());
throttler.reset(); // Clear throttle state
throttler.call(() => action()); // Executes immediatelyWorks with all state management solutions:
GetX:
ThrottledInkWell(
onTap: () => Get.find<MyController>().submit(),
child: Text("Submit"),
)Riverpod:
AsyncDebouncedTextController(
onChanged: (text) async {
return await ref.read(searchProvider.notifier).search(text);
},
onSuccess: (results) {
// Update state
},
)Bloc:
ThrottledBuilder(
builder: (context, throttle) {
return ElevatedButton(
onPressed: throttle(() => context.read<MyBloc>().add(SubmitEvent())),
child: Text("Submit"),
);
},
)Provider:
ThrottledInkWell(
onTap: () => context.read<CounterProvider>().increment(),
child: Text("Increment"),
)See more state management examples โ
Near-zero overhead:
| Metric | Performance |
|---|---|
| Throttle/Debounce | ~0.01ms per call |
| High-Frequency Throttler | ~0.001ms (100x faster) |
| Memory | ~40 bytes per controller |
Benchmarked: Handles 1000+ concurrent operations without frame drops.
See performance benchmarks โ
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new features
- Submit a pull request
See CONTRIBUTING.md for guidelines.
MIT License - See LICENSE file for details.
- ๐ฌ Questions: FAQ ยท GitHub Discussions
- ๐ Bugs: GitHub Issues
- โญ Like it? Star this repo!
Made with โค๏ธ for the Flutter community