From 07b1e42664ff4ad514ae893400050f701202728c Mon Sep 17 00:00:00 2001 From: Takuma Osada Date: Sun, 25 May 2025 16:55:35 +0900 Subject: [PATCH 1/2] feat: add hooks --- packages/flutter_hooks/lib/src/hooks.dart | 1 + packages/flutter_hooks/lib/src/throttled.dart | 42 +++++++++++ .../flutter_hooks/test/use_throttle_test.dart | 73 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 packages/flutter_hooks/lib/src/throttled.dart create mode 100644 packages/flutter_hooks/test/use_throttle_test.dart 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..bb059be --- /dev/null +++ b/packages/flutter_hooks/lib/src/throttled.dart @@ -0,0 +1,42 @@ +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 = useThrottle(duration: const Duration(milliseconds: 500)); +/// // Assume a fetch method fetchData(String query) exists +/// Button(onPressed: () => throttle(() => fetchData(userInput))); +/// ``` +void Function(VoidCallback callback) useThrottle({ + Duration duration = const Duration(milliseconds: 500), +}) { + final throttler = useMemoized(() => _Throttler(duration), [duration]); + return throttler.run; +} + +class _Throttler { + _Throttler(this.duration) + : assert( + 0 < duration.inMilliseconds, + 'duration must be greater than 0ms', + ); + + 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..3c9f2ef --- /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('useThrottle', () { + testWidgets('no update when tapping multiple times', (tester) async { + await tester.runAsync(() async { + await tester.pumpWidget(const _UseThrottleTestWidget()); + + 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 _UseThrottleTestWidget()); + + 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 _UseThrottleTestWidget extends HookWidget { + const _UseThrottleTestWidget(); + + @override + Widget build(BuildContext context) { + final textNumber = useState(1); + final throttle = useThrottle(); + + void updateText() { + textNumber.value++; + } + + return MaterialApp( + home: GestureDetector( + onTap: () => throttle(updateText), + child: Text(textNumber.value.toString()), + ), + ); + } +} + +const _duration = Duration(milliseconds: 500); From 4ebb7f701516ce0b68bad7eca880f86a0c86462a Mon Sep 17 00:00:00 2001 From: Takuma Osada Date: Thu, 5 Jun 2025 01:48:21 +0900 Subject: [PATCH 2/2] fixup! feat: add hooks --- packages/flutter_hooks/lib/src/throttled.dart | 12 ++++-------- packages/flutter_hooks/test/use_throttle_test.dart | 12 ++++++------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/flutter_hooks/lib/src/throttled.dart b/packages/flutter_hooks/lib/src/throttled.dart index bb059be..eda3544 100644 --- a/packages/flutter_hooks/lib/src/throttled.dart +++ b/packages/flutter_hooks/lib/src/throttled.dart @@ -7,23 +7,19 @@ part of 'hooks.dart'; /// String userInput = ''; // Your input value /// /// // Create a throttle callback -/// final throttle = useThrottle(duration: const Duration(milliseconds: 500)); +/// 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) useThrottle({ - Duration duration = const Duration(milliseconds: 500), +void Function(VoidCallback callback) useThrottled({ + required Duration duration, }) { final throttler = useMemoized(() => _Throttler(duration), [duration]); return throttler.run; } class _Throttler { - _Throttler(this.duration) - : assert( - 0 < duration.inMilliseconds, - 'duration must be greater than 0ms', - ); + _Throttler(this.duration); final Duration duration; diff --git a/packages/flutter_hooks/test/use_throttle_test.dart b/packages/flutter_hooks/test/use_throttle_test.dart index 3c9f2ef..8589f08 100644 --- a/packages/flutter_hooks/test/use_throttle_test.dart +++ b/packages/flutter_hooks/test/use_throttle_test.dart @@ -3,10 +3,10 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - group('useThrottle', () { + group('useThrottled', () { testWidgets('no update when tapping multiple times', (tester) async { await tester.runAsync(() async { - await tester.pumpWidget(const _UseThrottleTestWidget()); + await tester.pumpWidget(const _UseThrottledTestWidget()); final text = find.byType(GestureDetector); expect(find.text('1'), findsOneWidget); @@ -32,7 +32,7 @@ void main() { testWidgets('update number after duration', (tester) async { await tester.runAsync(() async { - await tester.pumpWidget(const _UseThrottleTestWidget()); + await tester.pumpWidget(const _UseThrottledTestWidget()); final text = find.byType(GestureDetector); expect(find.text('1'), findsOneWidget); @@ -49,13 +49,13 @@ void main() { }); } -class _UseThrottleTestWidget extends HookWidget { - const _UseThrottleTestWidget(); +class _UseThrottledTestWidget extends HookWidget { + const _UseThrottledTestWidget(); @override Widget build(BuildContext context) { final textNumber = useState(1); - final throttle = useThrottle(); + final throttle = useThrottled(duration: _duration); void updateText() { textNumber.value++;