The Problem You're Facing
Are you implementing debounce/throttle manually with Timer?
class _SearchState extends State<SearchPage> {
Timer? _debounceTimer;
void onSearchChanged(String query) {
_debounceTimer?.cancel();
_debounceTimer = Timer(Duration(milliseconds: 300), () {
_callSearchAPI(query);
});
}
@override
void dispose() {
_debounceTimer?.cancel(); // Forget this = memory leak!
super.dispose();
}
}
3 critical issues:
- ❌ Memory leak if you forget to dispose
- ❌ Race condition - old requests return after new ones
- ❌ Code duplication every time you need debounce
Solution: flutter_debounce_throttle
An all-in-one library with 360+ tests and 95% coverage that solves everything:
✅ Automatic lifecycle management
class _NewWayState extends State<NewWay> {
final debouncer = Debouncer(duration: 300.ms);
void search(String q) {
debouncer(() => api.search(q)); // That's it!
}
// No need to dispose anything!
}
✅ Proper async handling
final controller = ConcurrentAsyncThrottler(
mode: ConcurrencyMode.replace, // Auto cancel old requests
);
void onSearch(String query) {
controller(() async {
final results = await api.search(query);
setState(() => searchResults = results);
});
}
✅ Ready-to-use widgets
// Anti-spam button (1 line!)
ThrottledInkWell(
onTap: () => processPayment(),
child: Text('Pay \$99'),
)
// Search with loading state
DebouncedQueryBuilder<List<User>>(
onQuery: (text) async => await searchUsers(text),
builder: (context, search, isLoading) => TextField(onChanged: search),
)
Quick Comparison
| Feature | This lib | easy_debounce | rxdart | Manual Timer |
|---|---|---|---|---|
| Auto-dispose | ✅ | ❌ | ⚠️ | ❌ |
| Async support | ✅ | ❌ | ✅ | ❌ |
| Race condition handling | ✅ | ❌ | ⚠️ | ❌ |
| Rate Limiter | ✅ | ❌ | ❌ | ❌ |
| Flutter Widgets | ✅ | ❌ | ❌ | ❌ |
| Server-side (Pure Dart) | ✅ | ❌ | ❌ | ✅ |
| Dependencies | 0 | 0 | Many | 0 |
Real-World Use Cases
📱 Prevent double-click
ThrottledInkWell(
duration: 2.seconds,
onTap: () => processPayment(),
child: Text('Buy \$99'),
)
// User taps 10 times → only 1 is processed
🔍 Search autocomplete
final debouncer = Debouncer(duration: 300.ms);
TextField(onChanged: (text) => debouncer(() => search(text)))
// Only call API when user stops typing for 300ms
🖥️ Batch DB writes (Backend)
final batcher = BatchThrottler(
duration: 2.seconds,
maxBatchSize: 100,
onBatchExecute: (logs) => db.insertBatch(logs),
);
batcher(() => logEntry);
// 1000 calls → 10 batches → 90% less DB load
💰 Rate limit API
final limiter = RateLimiter(maxTokens: 1000, refillRate: 100);
if (!limiter.tryAcquire()) return 'Rate limit exceeded';
await callOpenAI(prompt);
Advanced Features
🏢 Distributed Rate Limiting (v2.4+)
Rate limiting across multiple server instances with Redis:
final limiter = DistributedRateLimiter(
key: 'user:$userId',
store: RedisRateLimiterStore(redis: redis),
maxTokens: 100,
refillRate: 10,
);
if (!await limiter.tryAcquire()) return Response(statusCode: 429);
🧹 Auto-cleanup (v2.3+)
Automatically cleanup unused limiters to prevent memory leaks with dynamic IDs:
class PostController with EventLimiterMixin {
void onLike(String postId) {
debounce('like_$postId', () => api.like(postId));
}
}
// Auto-remove unused limiters after 10 minutes
// User scrolls through 1000 posts - no OOM crash!
🎮 ThrottledGestureDetector
Drop-in replacement for GestureDetector with automatic throttling:
ThrottledGestureDetector(
onTap: () => handleTap(),
onPanUpdate: (details) => updatePosition(details.delta),
child: MyWidget(),
)
// Supports 40+ gesture callbacks with auto throttling!
Throttle vs Debounce?
Throttle: Execute immediately, lock for 300ms, ignore events during lock
- Use: Button clicks, scroll events
Debounce: Wait for user to stop for 300ms before executing
- Use: Search input, form validation
4 Concurrency Modes:
-
drop: Ignore new task if busy -
replace: Cancel old task, run new one (perfect for search) -
enqueue: Queue tasks -
keepLatest: Keep only running task + latest task
Mixin for State Management
Works with Provider, GetX, Bloc, Riverpod...
class SearchController extends GetxController with EventLimiterMixin {
void search(String query) {
debounceAsync('search', () async {
final results = await api.search(query);
update(results);
}, mode: ConcurrencyMode.replace);
}
@override
void onClose() {
cancelAll(); // Auto cleanup
super.onClose();
}
}
Production Quality
- ✅ 360+ tests with 95% coverage
- ✅ Zero dependencies (only
meta) - ✅ Type-safe (full generic support)
- ✅ Memory-safe (verified with LeakTracker)
- ✅ Cross-platform (Mobile, Web, Desktop, Server)
Installation
# Flutter App
dependencies:
flutter_debounce_throttle: ^2.4.2
# Flutter + Hooks
dependencies:
flutter_debounce_throttle_hooks: ^2.4.2
# Pure Dart (Server/CLI)
dependencies:
dart_debounce_throttle: ^2.4.2
Conclusion
If you're using:
- ❌ Manual
Timer→ Memory leak risk - ❌
easy_debounce→ Missing async support - ❌
rxdartonly for debounce → Overkill dependencies
Then flutter_debounce_throttle is the solution:
- ✅ Memory-safe by default
- ✅ Production-tested (360+ tests)
- ✅ Universal (Flutter + Dart backend)
- ✅ Zero dependencies
Links:
- 📦 Pub.dev: https://pub.dev/packages/flutter_debounce_throttle
- 📚 GitHub: https://github.com/brewkits/flutter_debounce_throttle
- 🐛 Issues: https://github.com/brewkits/flutter_debounce_throttle/issues
Technical review of flutter_debounce_throttle v2.4.2
Top comments (0)