diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index d1e4664..e193ca5 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -37,6 +37,7 @@ part 'scroll_controller.dart'; part 'search_controller.dart'; part 'tab_controller.dart'; part 'text_controller.dart'; +part 'throttled.dart'; part 'transformation_controller.dart'; part 'tree_sliver_controller.dart'; part 'widget_states_controller.dart'; diff --git a/packages/flutter_hooks/lib/src/throttled.dart b/packages/flutter_hooks/lib/src/throttled.dart new file mode 100644 index 0000000..eda3544 --- /dev/null +++ b/packages/flutter_hooks/lib/src/throttled.dart @@ -0,0 +1,38 @@ +part of 'hooks.dart'; + +/// widget ignore updates accordingly after a specified [duration] duration. +/// +/// Example: +/// ```dart +/// String userInput = ''; // Your input value +/// +/// // Create a throttle callback +/// final throttle = useThrottled(duration: const Duration(milliseconds: 500)); +/// // Assume a fetch method fetchData(String query) exists +/// Button(onPressed: () => throttle(() => fetchData(userInput))); +/// ``` +void Function(VoidCallback callback) useThrottled({ + required Duration duration, +}) { + final throttler = useMemoized(() => _Throttler(duration), [duration]); + return throttler.run; +} + +class _Throttler { + _Throttler(this.duration); + + final Duration duration; + + Timer? _timer; + + bool get _isRunning => _timer != null; + + void run(VoidCallback callback) { + if (!_isRunning) { + _timer = Timer(duration, () { + _timer = null; + }); + callback(); + } + } +} diff --git a/packages/flutter_hooks/test/use_throttle_test.dart b/packages/flutter_hooks/test/use_throttle_test.dart new file mode 100644 index 0000000..8589f08 --- /dev/null +++ b/packages/flutter_hooks/test/use_throttle_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('useThrottled', () { + testWidgets('no update when tapping multiple times', (tester) async { + await tester.runAsync(() async { + await tester.pumpWidget(const _UseThrottledTestWidget()); + + final text = find.byType(GestureDetector); + expect(find.text('1'), findsOneWidget); + + await tester.tap(text); + await tester.pump(); + + expect(find.text('2'), findsOneWidget); + + await tester.tap(text); + await tester.pump(); + expect(find.text('2'), findsOneWidget); + + await tester.tap(text); + await tester.pump(); + expect(find.text('2'), findsOneWidget); + + await tester.tap(text); + await tester.pump(); + expect(find.text('2'), findsOneWidget); + }); + }); + + testWidgets('update number after duration', (tester) async { + await tester.runAsync(() async { + await tester.pumpWidget(const _UseThrottledTestWidget()); + + final text = find.byType(GestureDetector); + expect(find.text('1'), findsOneWidget); + + await tester.pumpAndSettle(_duration); + await Future.delayed(_duration); + + await tester.tap(text); + await tester.pump(); + + expect(find.text('2'), findsOneWidget); + }); + }); + }); +} + +class _UseThrottledTestWidget extends HookWidget { + const _UseThrottledTestWidget(); + + @override + Widget build(BuildContext context) { + final textNumber = useState(1); + final throttle = useThrottled(duration: _duration); + + void updateText() { + textNumber.value++; + } + + return MaterialApp( + home: GestureDetector( + onTap: () => throttle(updateText), + child: Text(textNumber.value.toString()), + ), + ); + } +} + +const _duration = Duration(milliseconds: 500);