Skip to content

Commit e5ff3bd

Browse files
authored
Merge pull request #36 from DutchCodingCompany/feature/debouncer
Add debouncer and tests
2 parents 227301f + f065112 commit e5ff3bd

File tree

2 files changed

+238
-0
lines changed

2 files changed

+238
-0
lines changed

lib/debouncer/debouncer.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'dart:async';
2+
import 'dart:ui';
3+
4+
const _debounceTime = Duration(milliseconds: 400);
5+
6+
/// A debouncer is a utility class that allows you to debounce a function call.
7+
/// It is useful to prevent a function from being called too frequently.
8+
/// For example, if you have a function that is called when the user types in a search box,
9+
/// you can use a debouncer to prevent the function from being called too frequently.
10+
/// This is useful to prevent the server from being overwhelmed by too many requests.
11+
///
12+
/// Example:
13+
/// ```dart
14+
/// class SearchCubit extends Cubit<SearchState> {
15+
/// SearchCubit() : super(const SearchState());
16+
///
17+
/// late final Debouncer _debouncer = Debouncer();
18+
///
19+
/// void debouncedSearch(String searchQuery) {
20+
/// _debouncer.run(() => emit(state.copyWith(searchQuery: searchQuery.trim())));
21+
/// }
22+
///
23+
/// @override
24+
/// Future<void> close() {
25+
/// _debouncer.dispose();
26+
/// return super.close();
27+
/// }
28+
// }
29+
/// ```
30+
class Debouncer {
31+
/// Creates a new [Debouncer] with the given delay.
32+
/// If no delay is provided, the default delay of 400 milliseconds is used.
33+
Debouncer({Duration? delay}) : delay = delay ?? _debounceTime;
34+
35+
Timer? _timer;
36+
37+
/// The delay for the debouncer.
38+
final Duration delay;
39+
40+
/// Runs the given action after the delay.
41+
void run(VoidCallback action) {
42+
_timer?.cancel();
43+
44+
_timer = Timer(delay, action);
45+
}
46+
47+
/// Disposes the debouncer.
48+
void dispose() {
49+
_timer?.cancel();
50+
}
51+
}

test/debouncer/debouncer_test.dart

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import 'package:dcc_toolkit/debouncer/debouncer.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
void main() {
5+
group('Debouncer tests', () {
6+
late Debouncer debouncer;
7+
8+
tearDown(() {
9+
debouncer.dispose();
10+
});
11+
12+
group('constructor tests', () {
13+
test('should use default delay of 400ms when no delay is provided', () {
14+
debouncer = Debouncer();
15+
expect(debouncer.delay, equals(const Duration(milliseconds: 400)));
16+
});
17+
18+
test('should use custom delay when provided', () {
19+
const customDelay = Duration(milliseconds: 200);
20+
debouncer = Debouncer(delay: customDelay);
21+
expect(debouncer.delay, equals(customDelay));
22+
});
23+
});
24+
25+
group('run method tests', () {
26+
test('should execute action after default delay', () async {
27+
debouncer = Debouncer();
28+
var actionExecuted = false;
29+
30+
debouncer.run(() {
31+
actionExecuted = true;
32+
});
33+
34+
// Action should not be executed immediately
35+
expect(actionExecuted, isFalse);
36+
37+
// Wait for the default delay to pass
38+
await Future<void>.delayed(const Duration(milliseconds: 450));
39+
40+
// Action should now be executed
41+
expect(actionExecuted, isTrue);
42+
});
43+
44+
test('should execute action after custom delay', () async {
45+
const customDelay = Duration(milliseconds: 100);
46+
debouncer = Debouncer(delay: customDelay);
47+
var actionExecuted = false;
48+
49+
debouncer.run(() {
50+
actionExecuted = true;
51+
});
52+
53+
// Action should not be executed immediately
54+
expect(actionExecuted, isFalse);
55+
56+
// Wait less than the delay
57+
await Future<void>.delayed(const Duration(milliseconds: 50));
58+
expect(actionExecuted, isFalse);
59+
60+
// Wait for the custom delay to pass
61+
await Future<void>.delayed(const Duration(milliseconds: 60));
62+
63+
// Action should now be executed
64+
expect(actionExecuted, isTrue);
65+
});
66+
67+
test('should cancel previous timer when run is called multiple times', () async {
68+
debouncer = Debouncer(delay: const Duration(milliseconds: 100));
69+
var executionCount = 0;
70+
71+
// First call
72+
debouncer.run(() {
73+
executionCount++;
74+
});
75+
76+
// Wait less than delay
77+
await Future<void>.delayed(const Duration(milliseconds: 50));
78+
79+
// Second call should cancel the first
80+
debouncer.run(() {
81+
executionCount++;
82+
});
83+
84+
// Wait for the second delay to complete
85+
await Future<void>.delayed(const Duration(milliseconds: 120));
86+
87+
// Only the second action should have executed
88+
expect(executionCount, equals(1));
89+
});
90+
91+
test('should handle multiple rapid calls correctly', () async {
92+
debouncer = Debouncer(delay: const Duration(milliseconds: 100));
93+
var executionCount = 0;
94+
95+
// Make multiple rapid calls
96+
for (var i = 0; i < 5; i++) {
97+
debouncer.run(() {
98+
executionCount++;
99+
});
100+
await Future<void>.delayed(const Duration(milliseconds: 10));
101+
}
102+
103+
// Wait for delay to pass
104+
await Future<void>.delayed(const Duration(milliseconds: 120));
105+
106+
// Only the last action should have executed
107+
expect(executionCount, equals(1));
108+
});
109+
110+
test('should execute different actions correctly', () async {
111+
debouncer = Debouncer(delay: const Duration(milliseconds: 50));
112+
final results = <String>[];
113+
114+
debouncer.run(() {
115+
results.add('first');
116+
});
117+
118+
await Future<void>.delayed(const Duration(milliseconds: 25));
119+
120+
debouncer.run(() {
121+
results.add('second');
122+
});
123+
124+
await Future<void>.delayed(const Duration(milliseconds: 70));
125+
126+
expect(results, equals(['second']));
127+
});
128+
});
129+
130+
group('edge cases tests', () {
131+
test('should handle zero delay', () async {
132+
debouncer = Debouncer(delay: Duration.zero);
133+
var actionExecuted = false;
134+
135+
debouncer.run(() {
136+
actionExecuted = true;
137+
});
138+
139+
// Even with zero delay, action should execute asynchronously
140+
expect(actionExecuted, isFalse);
141+
142+
await Future<void>.delayed(const Duration(milliseconds: 1));
143+
144+
expect(actionExecuted, isTrue);
145+
});
146+
147+
test('should handle very long delay', () async {
148+
debouncer = Debouncer(delay: const Duration(seconds: 1));
149+
var actionExecuted = false;
150+
151+
debouncer.run(() {
152+
actionExecuted = true;
153+
});
154+
155+
await Future<void>.delayed(const Duration(milliseconds: 100));
156+
expect(actionExecuted, isFalse);
157+
158+
debouncer.dispose();
159+
});
160+
});
161+
162+
group('timing precision tests', () {
163+
test('should not execute action before delay', () async {
164+
debouncer = Debouncer(delay: const Duration(milliseconds: 100));
165+
var actionExecuted = false;
166+
167+
debouncer.run(() {
168+
actionExecuted = true;
169+
});
170+
171+
// Check at various points before delay
172+
await Future<void>.delayed(const Duration(milliseconds: 10));
173+
expect(actionExecuted, isFalse);
174+
175+
await Future<void>.delayed(const Duration(milliseconds: 40));
176+
expect(actionExecuted, isFalse);
177+
178+
await Future<void>.delayed(const Duration(milliseconds: 40));
179+
expect(actionExecuted, isFalse);
180+
181+
// Wait for completion
182+
await Future<void>.delayed(const Duration(milliseconds: 20));
183+
expect(actionExecuted, isTrue);
184+
});
185+
});
186+
});
187+
}

0 commit comments

Comments
 (0)