flutter_event_limiter 1.1.2
flutter_event_limiter: ^1.1.2 copied to clipboard
Throttle and debounce for Flutter. Prevent double-clicks, race conditions, memory leaks. Universal Builders for ANY widget with automatic loading states.
Flutter Event Limiter ๐ก๏ธ #
Production-ready throttle and debounce for Flutter.
Stop wrestling with Timer boilerplate, race conditions, and setState crashes. Handle Throttling (anti-spam), Debouncing (search APIs), and async operations with zero configuration.
Why developers love this package: โ Works with ANY widget (Material, Cupertino, custom) โ Auto-manages
mountedchecks andTimerdisposal โ Built-in loading states (no manual boolean flags) โ Race condition prevention (auto-cancels old API calls) โ 160/160 pub points ยท 95 tests ยท Zero dependencies
โก The 30-Second Demo #
The Old Way (Manual & Risky) #
15+ lines. Easy to forget dispose or mounted checks.
// โ Boilerplate & Error Prone
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; // Must remember this!
setState(() => _result = result);
} finally {
if (mounted) setState(() => _loading = false);
}
});
}
@override
void dispose() {
_timer?.cancel(); // Must remember this too!
super.dispose();
}
The New Way (Safe & Clean) #
3 lines. Auto-dispose. Auto-mounted check. Auto-loading state.
// โ
Clean & Safe
AsyncDebouncedTextController(
onChanged: (text) async => await api.search(text),
onSuccess: (result) => setState(() => _result = result),
onLoadingChanged: (loading) => setState(() => _loading = loading), // โจ Magic!
)
โจ Key Features #
| What You Get | Why It Matters |
|---|---|
| ๐งฉ Universal Builders | Works with Material, Cupertino, custom widgets. Never locked into specific UI components. |
| ๐ก๏ธ Auto-Safety | Zero setState crashes. Auto mounted checks. Auto Timer disposal. Production-tested. |
| โณ Smart Loading States | Built-in isLoading tracking. No manual bool isLoading = false needed. |
| ๐ Race Condition Prevention | Auto-cancels stale API calls. Search "flutter" โ only "flutter" results shown, never "flu". |
| ๐ Less Boilerplate | Compare 15 lines of manual Timer code vs 3 lines. See examples below. |
| ๐ฏ Production Ready | 95 comprehensive tests. Perfect pub score. Well-tested and reliable. |
๐ Usage Examples #
1. Prevent Button Double-Clicks (Throttle) #
Stop users from accidentally spamming payment buttons or API calls.
ThrottledInkWell(
onTap: () => submitOrder(), // ๐ Only runs once per 500ms
child: Text("Submit Order"),
)
// OR use with ANY widget:
ThrottledBuilder(
builder: (context, throttle) {
return CupertinoButton(
onPressed: throttle(() => submitOrder()),
child: Text("Submit"),
);
},
)
2. Smart Search Bar (Async Debounce) #
Waits for the user to stop typing. Automatically cancels old network requests to prevent wrong results.
AsyncDebouncedTextController(
duration: Duration(milliseconds: 300),
onChanged: (text) async => await api.search(text),
// โ
Only called if widget is mounted AND it's the latest result
onSuccess: (products) => setState(() => _products = products),
// โ
Auto-manage loading spinner
onLoadingChanged: (isLoading) => setState(() => _loading = isLoading),
)
3. Form Submission with Loading UI #
Disable the button and show a spinner while the async task runs.
AsyncThrottledCallbackBuilder(
onPressed: () async => await uploadFile(),
builder: (context, callback, isLoading) {
return ElevatedButton(
// Auto-disable button when loading
onPressed: isLoading ? null : callback,
child: isLoading
? CircularProgressIndicator()
: Text("Upload"),
);
},
)
๐ Feature Comparison #
How flutter_event_limiter improves upon common patterns:
| Feature | Raw Utility Libs | Stream Libs (Rx) | Hard-Coded Widget Libs | flutter_event_limiter |
|---|---|---|---|---|
| Approach | Manual | Reactive | Fixed Widgets | Builder Pattern |
| Works with Any Widget | โ | โ | โ | โ |
| Auto Mounted Check | โ | โ | โ | โ |
| Auto Loading State | โ | โ | โ | โ |
| Prevents Race Conditions | โ | โ | โ ๏ธ | โ |
| Setup Difficulty | Medium | Hard | Easy | Easy |
Note: While libraries like RxDart are powerful for complex stream transformations, flutter_event_limiter is optimized specifically for UI event handling with zero setup.
๐จ Universal Builder Pattern #
The Power of Flexibility: Unlike libraries that lock you into specific widgets, our builder pattern works with everything.
Example 1: Custom FAB with Throttle #
ThrottledBuilder(
duration: Duration(seconds: 1),
builder: (context, throttle) {
return FloatingActionButton(
onPressed: throttle(() => saveData()),
child: Icon(Icons.save),
);
},
)
Example 2: Cupertino Button with Debounce #
DebouncedBuilder(
duration: Duration(milliseconds: 500),
builder: (context, debounce) {
return CupertinoButton(
onPressed: debounce(() => updateSettings()),
child: Text("Save"),
);
},
)
Example 3: Custom Slider with Debounce #
DebouncedBuilder(
builder: (context, debounce) {
return Slider(
value: _volume,
onChanged: (value) => debounce(() {
setState(() => _volume = value);
api.updateVolume(value);
}),
);
},
)
Why this matters: Works with Material, Cupertino, Custom Widgets, or any third-party UI library.
๐ Complete Widget Reference #
๐ก๏ธ Throttling (Anti-Spam for Buttons) #
Executes immediately, then blocks for duration.
| Widget | Use Case |
|---|---|
ThrottledInkWell |
Buttons with Material ripple effect |
ThrottledTapWidget |
Buttons without ripple |
ThrottledBuilder |
Universal - Works with ANY widget |
AsyncThrottledCallbackBuilder |
Async operations with auto loading state |
AsyncThrottledCallback |
Async operations (manual mounted check) |
Throttler |
Direct class usage (advanced) |
When to use: Button clicks, form submissions, prevent spam clicks
โฑ๏ธ Debouncing (Search, Auto-save) #
Waits for pause, then executes.
| Widget | Use Case |
|---|---|
DebouncedTextController |
Basic text input debouncing |
AsyncDebouncedTextController |
Search API with auto-cancel & loading state |
DebouncedBuilder |
Universal - Works with ANY widget |
AsyncDebouncedCallbackBuilder |
Async with loading state |
Debouncer |
Direct class usage (advanced) |
When to use: Search input, auto-save, real-time validation
๐ฎ High-Frequency Events #
| Widget | Use Case |
|---|---|
HighFrequencyThrottler |
Scroll, mouse move, resize (60fps max, zero Timer overhead) |
๐ผ Real-World Scenarios #
๐ E-Commerce: Prevent Double Checkout #
Problem: User clicks "Place Order" twice during slow network โ Payment charged twice.
ThrottledInkWell(
onTap: () async => await placeOrder(),
child: Container(
padding: EdgeInsets.all(16),
color: Colors.green,
child: Text("Place Order - \$199.99"),
),
)
// โ
Second click ignored for 500ms - No duplicate orders
๐ Search with Race Condition Prevention #
Problem: User types "abc", API for "a" returns after "abc" โ Wrong results displayed.
AsyncDebouncedTextController(
duration: Duration(milliseconds: 300),
onChanged: (text) async => await searchProducts(text),
onSuccess: (products) => setState(() => _products = products),
onLoadingChanged: (loading) => setState(() => _searching = loading),
)
// โ
Old API calls auto-cancelled
// โ
Only latest result displayed
๐ Form with Auto Loading State #
Problem: No feedback during submission โ User clicks again โ Duplicate submit.
AsyncThrottledCallbackBuilder(
onPressed: () async {
await validateForm();
await submitForm();
if (!context.mounted) return;
Navigator.pop(context);
},
onError: (error, stack) => showSnackBar('Failed: $error'),
builder: (context, callback, isLoading) {
return ElevatedButton(
onPressed: isLoading ? null : callback,
child: isLoading ? CircularProgressIndicator() : Text("Submit"),
);
},
)
// โ
Button auto-disabled during submit
// โ
Loading indicator auto-managed
๐ฌ Chat App: Prevent Message Spam #
Problem: User presses Enter rapidly โ Sends duplicate messages.
ThrottledBuilder(
duration: Duration(seconds: 1),
builder: (context, throttle) {
return IconButton(
onPressed: throttle(() => sendMessage(_controller.text)),
icon: Icon(Icons.send),
);
},
)
// โ
Max 1 message per second
๐ Throttle vs Debounce: Which One? #
Throttle (Anti-Spam) #
Fires immediately, then ignores clicks for a duration.
User clicks: โผ โผ โผโผโผ โผ
Executes: โ X X X โ
|<-500ms->| |<-500ms->|
Use for: Buttons, Submits, Refresh actions
Debounce (Delay) #
Waits for a pause in action, then fires.
User types: a b c d ... (pause) ... e f g
Executes: โ โ
|<--300ms wait-->| |<--300ms wait-->|
Use for: Search bars, Auto-save, Slider values
AsyncDebouncer (Debounce + Auto-Cancel) #
Waits for pause + 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
๐ Migration from Other Libraries #
From easy_debounce #
Why migrate? Stop managing string IDs manually. Stop worrying about memory leaks.
// Before: Manual ID management, easy to forget dispose
import 'package:easy_debounce/easy_debounce.dart';
void onSearch(String text) {
EasyDebounce.debounce(
'search-tag', // โ Manage ID manually
Duration(milliseconds: 300),
() async {
final result = await api.search(text);
if (!mounted) return; // โ Easy to forget
setState(() => _results = result);
},
);
}
@override
void dispose() {
EasyDebounce.cancel('search-tag'); // โ Easy to forget
super.dispose();
}
// After: Auto-everything, 70% less code
AsyncDebouncedTextController(
onChanged: (text) async => await api.search(text),
onSuccess: (results) => setState(() => _results = results),
)
// โ
Auto-dispose, auto mounted check, no ID management
Benefits:
- โ No string ID management
- โ Auto-dispose (zero memory leaks)
- โ Built-in loading state
- โ Auto race condition prevention
From flutter_smart_debouncer #
Why migrate? Stop being locked into hard-coded widgets. Use ANY widget you want.
// Before: Locked to specific widget
SmartDebouncerButton(
onPressed: () => submit(),
child: Text("Submit"),
)
// โ What if you need CupertinoButton? FloatingActionButton? Custom widget?
// After: Universal builder - Use ANY widget
ThrottledBuilder(
builder: (context, throttle) {
return CupertinoButton( // Or FloatingActionButton, Custom, etc.
onPressed: throttle(() => submit()),
child: Text("Submit"),
);
},
)
Benefits:
- โ Works with ANY widget (Material, Cupertino, Custom)
- โ Not locked into specific UI components
- โ More flexible and future-proof
- โ Built-in loading state
From rxdart #
Why migrate? For simple UI events, you don't need Stream complexity.
// Before: 15+ lines with Stream/BehaviorSubject
final _searchController = BehaviorSubject<String>();
@override
void initState() {
super.initState();
_searchController.stream
.debounceTime(Duration(milliseconds: 300))
.listen((text) async {
final result = await api.search(text);
if (!mounted) return;
setState(() => _result = result);
});
}
@override
void dispose() {
_searchController.close();
super.dispose();
}
// After: 3 lines, no Stream knowledge needed
AsyncDebouncedTextController(
onChanged: (text) async => await api.search(text),
onSuccess: (result) => setState(() => _result = result),
)
Benefits:
- โ 80% less boilerplate
- โ No need to learn Streams/Subjects/Operators
- โ Auto mounted check (zero crashes)
- โ Flutter-first design (optimized for UI events)
When to still use RxDart:
- Complex reactive state management across multiple screens
- Need advanced operators (
combineLatest,switchMap, etc.) - Building reactive architecture (BLoC pattern)
๐ง Advanced Features #
Custom Durations #
ThrottledInkWell(
duration: Duration(seconds: 1), // Configurable
onTap: () => submit(),
child: Text('Submit'),
)
Reset Throttle Manually #
final throttler = Throttler();
InkWell(
onTap: throttler.wrap(() => handleTap()),
child: Text('Tap me'),
)
// Reset to allow immediate next call
throttler.reset();
// Check current state
if (throttler.isThrottled) {
print('Currently blocked');
}
Flush Debouncer (Execute Immediately) #
final controller = DebouncedTextController(
onChanged: (text) => search(text),
);
// User presses Enter โ Execute immediately without waiting
onSubmit() {
controller.flush(); // Cancels timer, executes now
}
Manual Cancel #
final debouncer = AsyncDebouncer();
// Start debounced operation
debouncer.run(() async => await api.call());
// Cancel all pending operations
debouncer.cancel();
โ ๏ธ Common Pitfalls #
1. Forgetting Mounted Check with Builder Widgets #
// โ BAD - Will crash if widget unmounts during async operation
AsyncDebouncedBuilder(
builder: (context, debounce) {
return TextField(
onChanged: (text) => debounce(() async {
final result = await api.search(text);
setState(() => _result = result); // โ Crash if unmounted!
}),
);
},
)
// โ
GOOD - Always check mounted
AsyncDebouncedBuilder(
builder: (context, debounce) {
return TextField(
onChanged: (text) => debounce(() async {
final result = await api.search(text);
if (!mounted) return; // โ
Safe
setState(() => _result = result);
}),
);
},
)
// โ
BETTER - Use CallbackBuilder for automatic mounted check
AsyncDebouncedCallbackBuilder(
onChanged: (text) async => await api.search(text),
onSuccess: (result) => setState(() => _result = result), // โ
Auto-checks mounted
builder: (context, callback, isLoading) => TextField(onChanged: callback),
)
2. Not Handling Null from AsyncDebouncer #
// โ BAD - Can crash if result is null (cancelled)
final result = await asyncDebouncer.run(() async => await api.call());
processResult(result); // โ Crash if cancelled
// โ
GOOD - Check for cancellation
final result = await asyncDebouncer.run(() async => await api.call());
if (result == null) return; // Operation was cancelled
processResult(result); // โ
Safe
3. Providing Both controller and initialValue #
// โ BAD - Will throw assertion error
DebouncedTextController(
controller: myController,
initialValue: "test", // โ Conflict!
onChanged: (text) => search(text),
)
// โ
GOOD - Use controller only
final controller = TextEditingController(text: "initial");
DebouncedTextController(
controller: controller,
onChanged: (text) => search(text),
)
// โ
GOOD - Use initialValue only
DebouncedTextController(
initialValue: "initial",
onChanged: (text) => search(text),
)
โ FAQ #
Q: Can I use this with GetX/Riverpod/Bloc? #
A: Yes! State-management agnostic.
// GetX
ThrottledInkWell(
onTap: () => Get.find<MyController>().submit(),
child: Text("Submit"),
)
// Riverpod
AsyncDebouncedTextController(
onChanged: (text) async => 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"),
);
},
)
Q: How to test widgets using this library? #
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'));
await tester.pump();
expect(clickCount, 1);
await tester.tap(find.text('Tap')); // Blocked
await tester.pump();
expect(clickCount, 1); // Still 1!
await tester.pumpAndSettle(Duration(milliseconds: 500));
await tester.tap(find.text('Tap')); // Works again
await tester.pump();
expect(clickCount, 2);
});
Q: Performance overhead? #
A: Near-zero! Benchmarked with 1000+ concurrent operations:
- Throttle/Debounce: ~0.01ms per call (faster than manual
Timer) - High-Frequency Throttler: ~0.001ms (100x faster, uses
DateTimeinstead ofTimer) - Memory: ~40 bytes per controller (same as a single
Timer) - Stress tested: Handles 100+ rapid calls without frame drops
๐ Installation #
Quick Start (30 seconds):
# Add to your project
flutter pub add flutter_event_limiter
# That's it! Zero configuration needed.
Or manually add to pubspec.yaml:
dependencies:
flutter_event_limiter: ^1.1.1
Import:
import 'package:flutter_event_limiter/flutter_event_limiter.dart';
๐ค Contributing #
We welcome contributions! Please feel free to check the issues or submit a PR.
- Bugs: Open an issue with a reproduction sample.
- Features: Discuss new features in issues before implementing.
๐ License #
MIT License - See LICENSE file for details.
๐ฎ Support #
- ๐ง Issues: GitHub Issues
- ๐ฌ Discussions: GitHub Discussions
- โญ Star this repo if you find it useful!
Made with โค๏ธ for the Flutter community