diff --git a/lib/debouncer/debouncer.dart b/lib/debouncer/debouncer.dart new file mode 100644 index 0000000..97fcd71 --- /dev/null +++ b/lib/debouncer/debouncer.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import 'dart:ui'; + +const _debounceTime = Duration(milliseconds: 400); + +/// A debouncer is a utility class that allows you to debounce a function call. +/// It is useful to prevent a function from being called too frequently. +/// For example, if you have a function that is called when the user types in a search box, +/// you can use a debouncer to prevent the function from being called too frequently. +/// This is useful to prevent the server from being overwhelmed by too many requests. +/// +/// Example: +/// ```dart +/// class SearchCubit extends Cubit { +/// SearchCubit() : super(const SearchState()); +/// +/// late final Debouncer _debouncer = Debouncer(); +/// +/// void debouncedSearch(String searchQuery) { +/// _debouncer.run(() => emit(state.copyWith(searchQuery: searchQuery.trim()))); +/// } +/// +/// @override +/// Future close() { +/// _debouncer.dispose(); +/// return super.close(); +/// } +// } +/// ``` +class Debouncer { + /// Creates a new [Debouncer] with the given delay. + /// If no delay is provided, the default delay of 400 milliseconds is used. + Debouncer({Duration? delay}) : delay = delay ?? _debounceTime; + + Timer? _timer; + + /// The delay for the debouncer. + final Duration delay; + + /// Runs the given action after the delay. + void run(VoidCallback action) { + _timer?.cancel(); + + _timer = Timer(delay, action); + } + + /// Disposes the debouncer. + void dispose() { + _timer?.cancel(); + } +} diff --git a/test/debouncer/debouncer_test.dart b/test/debouncer/debouncer_test.dart new file mode 100644 index 0000000..09c1e2c --- /dev/null +++ b/test/debouncer/debouncer_test.dart @@ -0,0 +1,187 @@ +import 'package:dcc_toolkit/debouncer/debouncer.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Debouncer tests', () { + late Debouncer debouncer; + + tearDown(() { + debouncer.dispose(); + }); + + group('constructor tests', () { + test('should use default delay of 400ms when no delay is provided', () { + debouncer = Debouncer(); + expect(debouncer.delay, equals(const Duration(milliseconds: 400))); + }); + + test('should use custom delay when provided', () { + const customDelay = Duration(milliseconds: 200); + debouncer = Debouncer(delay: customDelay); + expect(debouncer.delay, equals(customDelay)); + }); + }); + + group('run method tests', () { + test('should execute action after default delay', () async { + debouncer = Debouncer(); + var actionExecuted = false; + + debouncer.run(() { + actionExecuted = true; + }); + + // Action should not be executed immediately + expect(actionExecuted, isFalse); + + // Wait for the default delay to pass + await Future.delayed(const Duration(milliseconds: 450)); + + // Action should now be executed + expect(actionExecuted, isTrue); + }); + + test('should execute action after custom delay', () async { + const customDelay = Duration(milliseconds: 100); + debouncer = Debouncer(delay: customDelay); + var actionExecuted = false; + + debouncer.run(() { + actionExecuted = true; + }); + + // Action should not be executed immediately + expect(actionExecuted, isFalse); + + // Wait less than the delay + await Future.delayed(const Duration(milliseconds: 50)); + expect(actionExecuted, isFalse); + + // Wait for the custom delay to pass + await Future.delayed(const Duration(milliseconds: 60)); + + // Action should now be executed + expect(actionExecuted, isTrue); + }); + + test('should cancel previous timer when run is called multiple times', () async { + debouncer = Debouncer(delay: const Duration(milliseconds: 100)); + var executionCount = 0; + + // First call + debouncer.run(() { + executionCount++; + }); + + // Wait less than delay + await Future.delayed(const Duration(milliseconds: 50)); + + // Second call should cancel the first + debouncer.run(() { + executionCount++; + }); + + // Wait for the second delay to complete + await Future.delayed(const Duration(milliseconds: 120)); + + // Only the second action should have executed + expect(executionCount, equals(1)); + }); + + test('should handle multiple rapid calls correctly', () async { + debouncer = Debouncer(delay: const Duration(milliseconds: 100)); + var executionCount = 0; + + // Make multiple rapid calls + for (var i = 0; i < 5; i++) { + debouncer.run(() { + executionCount++; + }); + await Future.delayed(const Duration(milliseconds: 10)); + } + + // Wait for delay to pass + await Future.delayed(const Duration(milliseconds: 120)); + + // Only the last action should have executed + expect(executionCount, equals(1)); + }); + + test('should execute different actions correctly', () async { + debouncer = Debouncer(delay: const Duration(milliseconds: 50)); + final results = []; + + debouncer.run(() { + results.add('first'); + }); + + await Future.delayed(const Duration(milliseconds: 25)); + + debouncer.run(() { + results.add('second'); + }); + + await Future.delayed(const Duration(milliseconds: 70)); + + expect(results, equals(['second'])); + }); + }); + + group('edge cases tests', () { + test('should handle zero delay', () async { + debouncer = Debouncer(delay: Duration.zero); + var actionExecuted = false; + + debouncer.run(() { + actionExecuted = true; + }); + + // Even with zero delay, action should execute asynchronously + expect(actionExecuted, isFalse); + + await Future.delayed(const Duration(milliseconds: 1)); + + expect(actionExecuted, isTrue); + }); + + test('should handle very long delay', () async { + debouncer = Debouncer(delay: const Duration(seconds: 1)); + var actionExecuted = false; + + debouncer.run(() { + actionExecuted = true; + }); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(actionExecuted, isFalse); + + debouncer.dispose(); + }); + }); + + group('timing precision tests', () { + test('should not execute action before delay', () async { + debouncer = Debouncer(delay: const Duration(milliseconds: 100)); + var actionExecuted = false; + + debouncer.run(() { + actionExecuted = true; + }); + + // Check at various points before delay + await Future.delayed(const Duration(milliseconds: 10)); + expect(actionExecuted, isFalse); + + await Future.delayed(const Duration(milliseconds: 40)); + expect(actionExecuted, isFalse); + + await Future.delayed(const Duration(milliseconds: 40)); + expect(actionExecuted, isFalse); + + // Wait for completion + await Future.delayed(const Duration(milliseconds: 20)); + expect(actionExecuted, isTrue); + }); + }); + }); +}