Line data Source code
1 : import 'dart:async';
2 : import 'package:flutter/foundation.dart';
3 : import 'debouncer_config.dart';
4 : import 'strategy/debounce_strategy.dart';
5 : import 'strategy/trailing_edge_strategy.dart';
6 : import 'utils/debouncer_logger.dart';
7 : import 'utils/disposable.dart';
8 : import 'utils/lifecycle_hooks.dart';
9 :
10 : /// A flexible, strategy-based debouncer for Flutter and Dart.
11 : ///
12 : /// [Debouncer] delays executing an action until a specified [delay] has
13 : /// passed since the last invocation. The firing behaviour is controlled
14 : /// by a pluggable [DebouncerStrategy].
15 : ///
16 : /// ## Basic usage
17 : /// ```dart
18 : /// final _debouncer = Debouncer(delay: Duration(milliseconds: 300));
19 : ///
20 : /// void onSearchChanged(String query) {
21 : /// _debouncer.run(() => _fetchResults(query));
22 : /// }
23 : ///
24 : /// @override
25 : /// void dispose() {
26 : /// _debouncer.dispose();
27 : /// super.dispose();
28 : /// }
29 : /// ```
30 : ///
31 : /// ## Leading-edge (fires immediately, then locks)
32 : /// ```dart
33 : /// final _debouncer = Debouncer(
34 : /// delay: Duration(milliseconds: 500),
35 : /// strategy: LeadingEdgeStrategy(),
36 : /// );
37 : /// ```
38 : ///
39 : /// ## With logging
40 : /// ```dart
41 : /// final _debouncer = Debouncer(
42 : /// delay: Duration(milliseconds: 300),
43 : /// logger: DebouncerLogger(level: DebouncerLogLevel.verbose),
44 : /// );
45 : /// ```
46 : class Debouncer with Disposable {
47 : /// The active configuration (delay, label, edge flags).
48 : final DebouncerConfig config;
49 :
50 : /// The strategy that decides when and how the action fires.
51 : final DebouncerStrategy strategy;
52 :
53 : /// Optional logger for debugging debounce events.
54 : final DebouncerLogger? logger;
55 :
56 : /// Optional lifecycle hooks (onFire, onCancel, onDispose).
57 : final LifecycleHooks? hooks;
58 :
59 : Timer? _timer;
60 :
61 : /// Creates a [Debouncer] with the given [delay] and optional overrides.
62 : ///
63 : /// - [delay] — how long to wait after the last call. Default: 300 ms.
64 : /// - [DebouncerStrategy] — firing behaviour. Default: [TrailingEdgeStrategy].
65 : /// - [logger] — attach a [DebouncerLogger] to trace events.
66 : /// - [hooks] — lifecycle callbacks (onFire, onCancel, onDispose).
67 : /// - [label] — a debug label shown in log output.
68 1 : Debouncer({
69 : Duration delay = const Duration(milliseconds: 300),
70 : DebouncerStrategy? strategy,
71 : this.logger,
72 : this.hooks,
73 : String? label,
74 1 : }) : config = DebouncerConfig(delay: delay, debugLabel: label),
75 : strategy = strategy ?? const TrailingEdgeStrategy();
76 :
77 :
78 :
79 : /// Creates a [Debouncer] directly from a [DebouncerConfig].
80 1 : Debouncer.fromConfig(
81 : this.config, {
82 : DebouncerStrategy? strategy,
83 : this.logger,
84 : this.hooks,
85 : }) : strategy = strategy ?? const TrailingEdgeStrategy();
86 :
87 : /// Whether a call is currently pending (timer is running).
88 8 : bool get isPending => _timer != null && _timer!.isActive;
89 :
90 : /// Schedules [action] to run according to the active [DebouncerStrategy].
91 : ///
92 : /// Each call resets the internal timer (for trailing-edge strategies).
93 : /// Throws a [StateError] if called after [dispose].
94 2 : void run(VoidCallback action) {
95 2 : assertNotDisposed('run');
96 :
97 2 : final bool hadPendingTimer = isPending;
98 :
99 4 : strategy.execute(
100 2 : () {
101 5 : logger?.logFire(config.debugLabel);
102 4 : hooks?.onFire?.call();
103 2 : action();
104 : },
105 2 : config,
106 2 : _timer,
107 2 : (t) {
108 2 : _timer = t;
109 : if (t != null) {
110 7 : logger?.logReset(config.debugLabel, config.delay);
111 : }
112 : },
113 : );
114 :
115 : if (hadPendingTimer) {
116 5 : logger?.logCancel(config.debugLabel);
117 4 : hooks?.onCancel?.call();
118 : }
119 : }
120 :
121 : /// Cancels any pending action without executing it.
122 1 : void cancel() {
123 1 : if (isPending) {
124 2 : _timer?.cancel();
125 1 : _timer = null;
126 4 : logger?.logCancel(config.debugLabel);
127 1 : hooks?.onCancel?.call();
128 : }
129 : }
130 :
131 : /// Cancels any pending timer and marks this instance as disposed.
132 : ///
133 : /// Always call [dispose] in [State.dispose] to prevent timer leaks.
134 2 : @override
135 : void onDispose() {
136 4 : _timer?.cancel();
137 2 : _timer = null;
138 4 : hooks?.onDispose?.call();
139 : }
140 :
141 0 : @override
142 : String toString() =>
143 0 : 'Debouncer(config: $config, isPending: $isPending, disposed: $isDisposed)';
144 : }
|