debouncing 1.1.0
debouncing: ^1.1.0 copied to clipboard
A lightweight Flutter package providing a simple [debounce, throttle, back off] utility to function calls, ideal for optimizing performance in event-driven application.
Debounce, Throttle, BackOff #
A lightweight Flutter package providing a simple [debounce, throttle, back off] utility to function calls, ideal for optimizing performance in event-driven application.
Demo #
![]() |
![]() |
---|
Features #
- Simple and easy-to-use functionality with (
ThrottleMixins
,DebounceMixins
). Mixins help you to closeThrottle
andDebounce
automatically, and it'snot necessary
to close it manually. - When using
debounceTransform
orthrottleTransform
inBloc
, you can now cancel the previous event usingResettableEvent
. - Configurable [
duration
,leading
,trailing
] inDebounce
andThrottle
. - Configurable [
percentageRandomization
,initialDelay
,maxDelay
,maxAttempts
,retryIf
] inBackOff
. - Ideal for Flutter applications to handle rapid user inputs (e.g., search fields, button clicks).
Installation #
Add the following to your pubspec.yaml
file:
dependencies:
debouncing: ^[latest_version]
Then run:
flutter pub get
Usage Example #
event transformers for [Bloc] #
import 'package:debouncing/debouncing.dart';
/// 👉 Usual example of event:
sealed class MyEvent {
const MyEvent();
}
// final class OnSearchEvent extends MyEvent { 👉 // ⚠️ you can write without `ResettableEvent`
final class OnSearchEvent extends MyEvent with ResettableEvent { 👉 // ⚠️ ... with ResettableEvent, ... extends ResettableEvent, ... implements ResettableEvent
@override
final bool resetOnlyPreviousEvent;
final String text;
const OnSearchEvent({required this.text, this.resetOnlyPreviousEvent = false});
}
// final class OnScrollEvent extends MyEvent { 👉 // ⚠️ you can write without `ResettableEvent`
final class OnScrollEvent extends MyEvent with ResettableEvent { 👉 // ⚠️ ... with ResettableEvent, ... extends ResettableEvent, ... implements ResettableEvent
@override
final bool resetOnlyPreviousEvent;
const OnScrollEvent({this.resetOnlyPreviousEvent = false});
}
----------------------------------------------------------------------------------------------------
/// 👉 Freezed example of event:
@freezed
abstract class MyEvent with _$MyEvent {
// const factory MyEvent.onSearchEvent({ 👉 // ⚠️ you can write without `@Implements<ResettableEvent>()`
@Implements<ResettableEvent>()
const factory MyEvent.onSearchEvent({
required String text,
@Default(false) bool resetOnlyPreviousEvent,
}) = OnSearchEvent;
// const factory MyEvent.onScroll({ 👉 // ⚠️ you can write without `@Implements<ResettableEvent>()`
@Implements<ResettableEvent>()
const factory MyEvent.onScroll({
@Default(false) bool resetOnlyPreviousEvent,
}) = OnScrollEvent;
}
----------------------------------------------------------------------------------------------------
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc() : super(MyStateInitial()) {
on<OnSearchEvent>(
_onSearch,
transformer: debounceTransform(
delay: const Duration(milliseconds: 300),
leading: false,
trailing: true,
),
);
on<OnScrollEvent>(
_onScroll,
transformer: throttleTransform(
interval: const Duration(milliseconds: 300),
leading: true,
trailing: false,
),
);
}
void _onScroll(OnScroll event, Emitter emit) {
// ...
}
void _onSearch(OnTextChanged event, Emitter emit) {
// ...
}
}
mixin for [Bloc and Cubit] #
import 'package:debouncing/debouncing.dart';
class MyBloc extends Bloc<MyEvent, MyState> with DebounceMixin, ThrottleMixin {
MyBloc() : super(MyState()) {
on<OnScroll>(_onScroll);
on<OnTextChanged>(_onTextChanged);
}
void _onScroll(OnScroll event, Emitter emit) {
throttle(() {
emit(MyNewState());
// ...
});
}
void _onSearch(OnTextChanged event, Emitter emit) {
debounce(() {
emit(MyNewState());
// ...
});
}
}
-------------------------------------------------------------------------------------------
class MyCubit extends Cubit<MyState> with DebounceMixin, ThrottleMixin {
MyCubit() : super(const MyState());
void onSearch(String text) {
debounce(() {
emit(MyNewState());
// ...
});
}
void onScroll() {
throttle(() {
emit(MyNewState());
// ...
});
}
}
-------------------------------------------------------------------------------------------
/// 👉 You can write mixin for [Bloc] and [Cubit] like this.
/// If you need full version mixin with [Documentation] and [Usage example] for your project,
/// you can take it from file (lib/src/debouncing/mixins/debounce_bloc_and_cubit_mixin.dart)
mixin DebounceMixin<T> on BlocBase<T> {
@protected
@visibleForTesting
DebounceParams get debounceParams => const DebounceParams();
@protected
@nonVirtual
@visibleForTesting
late final Debounce debounce = Debounce(
delay: debounceParams.delay,
leading: debounceParams.leading,
trailing: debounceParams.trailing,
);
@override
@protected
@mustCallSuper
Future<void> close() {
debounce.dispose();
return super.close();
}
}
/// 👉 You can write mixin for [Bloc] and [Cubit] like this.
/// If you need full version mixin with [Documentation] and [Usage example] for your project,
/// you can take it from file (lib/src/throttling/mixins/throttle_bloc_and_cubit_mixin.dart)
mixin ThrottleMixin<T> on BlocBase<T> {
@protected
@visibleForTesting
ThrottleParams get throttleParams => const ThrottleParams();
@protected
@nonVirtual
@visibleForTesting
late final Throttle throttle = Throttle(
interval: throttleParams.interval,
leading: throttleParams.leading,
trailing: throttleParams.trailing,
);
@override
@protected
@mustCallSuper
Future<void> close() {
throttle.dispose();
return super.close();
}
}
mixin for [Provider] and other classes inherited from 'ChangeNotifier' #
import 'package:debouncing/debouncing.dart';
class MyProvider with ChangeNotifier, ThrottleNotifierMixin, DebounceNotifierMixin {
void onScroll() {
throttle(() {
// ...
notifyListeners();
});
}
void onSearch(String query) {
debounce(() {
// ...
notifyListeners();
});
}
}
mixin for [StatefulWidget] #
import 'package:flutter/material.dart';
import 'package:debouncing/debouncing.dart';
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with ThrottleStateMixin, DebounceStateMixin {
void onScroll() {
throttle(() {
// ...
});
}
void _onSearch(String value) {
debounce(() {
// ...
});
}
@override
Widget build(BuildContext context) {
// ...
}
}
mixin for [GetX] #
import 'package:debouncing/debouncing.dart';
import 'package:get/get.dart' hide debounce;
class MyController extends GetxController with DebounceGetXMixin, ThrottleGetXMixin {
void onSearch(String query) {
debounce(() {
// ...
update();
});
}
void onScroll(String query) {
throttle(() {
// ...
update();
});
}
}
/// 👉 You can write mixin for [GetxController] and [GetxService] like this.
/// If you need full version mixin with [Documentation] and [Usage example] for your project,
/// you can take it from file (lib/src/debouncing/mixins/debounce_getx_mixin.dart)
mixin DebounceGetXMixin on GetLifeCycleBase {
@protected
@visibleForTesting
DebounceParams get debounceParams => const DebounceParams();
@protected
@nonVirtual
@visibleForTesting
late final Debounce debounce = Debounce(
delay: debounceParams.delay,
leading: debounceParams.leading,
trailing: debounceParams.trailing,
);
@override
@protected
@mustCallSuper
void onClose() {
debounce.dispose();
super.onClose();
}
}
/// 👉 You can write mixin for [GetxController] and [GetxService] like this.
/// If you need full version mixin with [Documentation] and [Usage example] for your project,
/// you can take it from file (lib/src/throttling/mixins/throttle_getx_mixin.dart)
mixin ThrottleGetXMixin on GetLifeCycleBase {
@protected
@visibleForTesting
ThrottleParams get throttleParams => const ThrottleParams();
@protected
@nonVirtual
@visibleForTesting
late final Throttle throttle = Throttle(
interval: throttleParams.interval,
leading: throttleParams.leading,
trailing: throttleParams.trailing,
);
@override
@protected
@mustCallSuper
void onClose() {
throttle.dispose();
super.onClose();
}
}
Reference #
Debounce
, Throttle
and EventTransformers [debounceTransform
, throttleTransform
] for Bloc
#
/// source.debounceTransform(delay: Duration(seconds: 1), leading: false, trailing: true); ✅ Good!
/// Config: leading: false, trailing: true
/// Input: 1-2-3---4---5-6-|
/// Output: ------3---4-----6|
///
/// source.debounceTransform(delay: Duration(seconds: 1), leading: true, trailing: false); ✅ Good!
/// Config: leading: true, trailing: false
/// Input: 1-2-3---4---5-6-|
/// Output: 1-------4---5---|
///
/// source.debounceTransform(delay: Duration(seconds: 1), leading: true, trailing: true); ✅ Good!
/// Config: leading: true, trailing: true
/// Input: 1-2-3---4---5-6-|
/// Output: 1-----3-4---5---6|
///
/// source.debounceTransform(delay: Duration(seconds: 1), leading: false, trailing: false); ❌ Bad! Output empty!
/// Config: leading: false, trailing: false
/// Input: 1-2-3---4---5-6-|
/// Output: ----------------|
on<OnSearchEvent>(
_onSearchEvent,
transformer: debounceTransform(
delay: const Duration(seconds: 1),
leading: false,
trailing: true,
),
);
-----------------------------------------------------------------------------------------------------------------------
/// source.throttleTransform(interval: const Duration(seconds: 6), leading: true, trailing: false); ✅ Good!
/// Config: leading: true, trailing: false
/// Input: 1-2-3---4-5-6---7-8-|
/// Output: 1-------4-------7---|
///
/// source.throttleTransform(interval: const Duration(seconds: 6), leading: false, trailing: true); ✅ Good!
/// Config: leading: false, trailing: true
/// Input: 1-2-3---4-5----6--|
/// Output: ------3-----5-----6|
///
/// source.throttleTransform(interval: const Duration(seconds: 6), leading: true, trailing: true); ✅ Good!
/// Config: leading: true, trailing: true
/// Input: 1-2-----3-----4|
/// Output: 1-----2-----3--|
///
/// source.throttleTransform(interval: const Duration(seconds: 6), leading: false, trailing: false); ❌ Bad! Output empty!
/// Config: leading: false, trailing: false
/// Input: 1-2-3---4-5----6--|
/// Output: -------------------|
on<OnScrollEvent>(
_onScrollEvent,
transformer: throttleTransform(
interval: const Duration(seconds: 6),
leading: true,
trailing: false,
),
);
/// final _debounce = Debounce(delay: Duration(seconds: 1), leading: false, trailing: true); ✅ Good!
/// Config: leading: false, trailing: true
/// Input: 1-2-3---4---5-6-|
/// Output: ------3---4-----6|
///
/// final _debounce = Debounce(delay: Duration(seconds: 1), leading: true, trailing: false); ✅ Good!
/// Config: leading: true, trailing: false
/// Input: 1-2-3---4---5-6-|
/// Output: 1-------4---5---|
///
/// final _debounce = Debounce(delay: Duration(seconds: 1), leading: true, trailing: true); ✅ Good!
/// Config: leading: true, trailing: true
/// Input: 1-2-3---4---5-6-|
/// Output: 1-----3-4---5---6|
///
/// final _debounce = Debounce(delay: Duration(seconds: 1), leading: false, trailing: false); ❌ Bad! Output empty!
/// Config: leading: false, trailing: false
/// Input: 1-2-3---4---5-6-|
/// Output: ----------------|
final _debounce = Debounce();
-----------------------------------------------------------------------------------------------------------------------
/// final _throttle = Throttle(interval: const Duration(seconds: 6), leading: true, trailing: false); ✅ Good!
/// Config: leading: true, trailing: false
/// Input: 1-2-3---4-5-6---7-8-|
/// Output: 1-------4-------7---|
///
/// final _throttle = Throttle(interval: const Duration(seconds: 6), leading: false, trailing: true); ✅ Good!
/// Config: leading: false, trailing: true
/// Input: 1-2-3---4-5----6--|
/// Output: ------3-----5-----6|
///
/// final _throttle = Throttle(interval: const Duration(seconds: 6), leading: true, trailing: true); ✅ Good!
/// Config: leading: true, trailing: true
/// Input: 1-2-----3-----4|
/// Output: 1-----2-----3--|
///
/// final _throttle = Throttle(interval: const Duration(seconds: 6), leading: false, trailing: false); ❌ Bad! Output empty!
/// Config: leading: false, trailing: false
/// Input: 1-2-3---4-5----6--|
/// Output: -------------------|
final _throttle = Throttle();
callback
: — The function to debounce or throttle.interval
,delay
: — the interval at which the function can be calledleading
: — Iftrue
, [callback] will be called on the first call before the interval expires.trailing
: — Iftrue
, [callback] will be called after the interval endsleading
&&trailing
: If both aretrue
, [leading]callback
will be called immediately before the interval expires and [trailing]callback
will be called after the interval ends (if there were repeated calls)
BackOff
#
[BackOff] is used for holding options for retrying a function.
/// With the default configuration functions will be retried up-to 7 times
/// (8 attempts in total), sleeping 1st, 2nd, 3rd, ..., 7th attempt:
/// 1. 400 ms ± 25%
/// 2. 800 ms ± 25%
/// 3. 1600 ms ± 25%
/// 4. 3200 ms ± 25%
/// 5. 6400 ms ± 25%
/// 6. 12800 ms ± 25%
/// 7. 25600 ms ± 25%
final response = await BackOff(
() => http.get('https://google.com').timeout(
const Duration(seconds: 10),
),
retryIf: (error, stackTrace, attempt) => error is SocketException || error is TimeoutException,
initialDelay: const Duration(milliseconds: 200),
maxAttempts: 8,
percentageRandomization: 0.25,
maxDelay: const Duration(seconds: 30),
).call();
initialDelay
: — Defaults to 200 ms, which results in the following delays. Delay factor to double after every attempt.percentageRandomization
: — Percentage the delay should be randomized, given as fraction between 0 and 1, (0.0 to 1.0 recommended). If [percentageRandomization] is0.25
(default) this indicates 25 % of the delay should be increased or decreased by 25 %.maxDelay
: — Maximum delay between retries, defaults to 30 seconds.maxAttempts
: — Maximum number of attempts before giving up, defaults to 8.retryIf
: — Function to determine if a retry should be attempted. Ifnull
(default) all errors will be retried.
Contributing #
Contributions are welcome! Please follow these steps:
- Fork the repository.
- Create a new branch (
git checkout -b feature/your-feature
). - Commit your changes (
git commit -m 'Add your feature'
). - Push to the branch (
git push origin feature/your-feature
). - Open a Pull Request.
Contacts #
For issues or suggestions, please open an issue on the GitHub repository.