diff --git a/README.md b/README.md index c28a1583..6aaefa04 100644 --- a/README.md +++ b/README.md @@ -352,6 +352,7 @@ A series of hooks with no particular theme. | [useIsMounted](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useIsMounted.html) | An equivalent to `State.mounted` for hooks. | | [useAutomaticKeepAlive](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAutomaticKeepAlive.html) | An equivalent to the `AutomaticKeepAlive` widget for hooks. | | [useOnPlatformBrightnessChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnPlatformBrightnessChange.html) | Listens to platform `Brightness` changes and triggers a callback on change.| +| [useSearchController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSearchController.html) | Creates and disposes a `SearchController`. | ## Contributions diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md index 4cd7f74c..23f73e76 100644 --- a/packages/flutter_hooks/CHANGELOG.md +++ b/packages/flutter_hooks/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased minor + +- Added `useSearchController` (thanks to @snapsl) + ## 0.18.6 - Added korean translation (thanks to @sejun2) diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 8b8cb7c4..036289fd 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' show Brightness, TabController; +import 'package:flutter/material.dart' + show Brightness, SearchController, TabController; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart'; @@ -9,17 +10,18 @@ import 'framework.dart'; part 'animation.dart'; part 'async.dart'; +part 'focus_node.dart'; +part 'focus_scope_node.dart'; +part 'keep_alive.dart'; part 'listenable.dart'; +part 'listenable_selector.dart'; part 'misc.dart'; +part 'page_controller.dart'; +part 'platform_brightness.dart'; part 'primitives.dart'; +part 'scroll_controller.dart'; +part 'search_controller.dart'; part 'tab_controller.dart'; part 'text_controller.dart'; -part 'focus_node.dart'; -part 'focus_scope_node.dart'; -part 'scroll_controller.dart'; -part 'page_controller.dart'; -part 'widgets_binding_observer.dart'; part 'transformation_controller.dart'; -part 'platform_brightness.dart'; -part 'keep_alive.dart'; -part 'listenable_selector.dart'; +part 'widgets_binding_observer.dart'; diff --git a/packages/flutter_hooks/lib/src/search_controller.dart b/packages/flutter_hooks/lib/src/search_controller.dart new file mode 100644 index 00000000..454ea6b0 --- /dev/null +++ b/packages/flutter_hooks/lib/src/search_controller.dart @@ -0,0 +1,31 @@ +part of 'hooks.dart'; + +/// Creates a [SearchController] that will be disposed automatically. +/// +/// See also: +/// - [SearchController] +SearchController useSearchController({List? keys}) { + return use(_SearchControllerHook(keys: keys)); +} + +class _SearchControllerHook extends Hook { + const _SearchControllerHook({List? keys}) : super(keys: keys); + + @override + HookState> createState() => + _SearchControllerHookState(); +} + +class _SearchControllerHookState + extends HookState { + final controller = SearchController(); + + @override + String get debugLabel => 'useSearchController'; + + @override + SearchController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); +} diff --git a/packages/flutter_hooks/test/use_search_controller_test.dart b/packages/flutter_hooks/test/use_search_controller_test.dart new file mode 100644 index 00000000..10061ae1 --- /dev/null +++ b/packages/flutter_hooks/test/use_search_controller_test.dart @@ -0,0 +1,92 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/src/framework.dart'; +import 'package:flutter_hooks/src/hooks.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useSearchController(); + + return const SizedBox(); + }), + ); + + await tester.pump(); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useSearchController:\n' + ' │ SearchController#00000(TextEditingValue(text: ┤├, selection:\n' + ' │ TextSelection.invalid, composing: TextRange(start: -1, end:\n' + ' │ -1)))\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useSearchController', () { + testWidgets('initial values matches with real constructor', (tester) async { + late SearchController controller; + final controller2 = SearchController(); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useSearchController(); + + return Container(); + }), + ); + + expect(controller, isA()); + + expect(controller.selection, controller2.selection); + expect(controller.text, controller2.text); + expect(controller.value, controller2.value); + }); + + testWidgets('check opening/closing view', (tester) async { + late SearchController controller; + + await tester.pumpWidget(MaterialApp( + home: HookBuilder(builder: (context) { + controller = useSearchController(); + + return SearchAnchor.bar( + searchController: controller, + suggestionsBuilder: (context, controller) => [], + ); + }), + )); + + controller.openView(); + + expect(controller.isOpen, true); + + await tester.pumpWidget(MaterialApp( + home: HookBuilder(builder: (context) { + controller = useSearchController(); + + return SearchAnchor.bar( + searchController: controller, + suggestionsBuilder: (context, controller) => [], + ); + }), + )); + + controller.closeView('selected'); + + expect(controller.isOpen, false); + expect(controller.text, 'selected'); + }); + }); +}