diff --git a/README.md b/README.md index 99aff23..6e4395b 100644 --- a/README.md +++ b/README.md @@ -358,6 +358,7 @@ A series of hooks with no particular theme. | [useExpansionTileController](https://api.flutter.dev/flutter/material/ExpansionTileController-class.html) | Creates a `ExpansionTileController`. | | [useDebounced](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useDebounced.html) | Returns a debounced version of the provided value, triggering widget updates accordingly after a specified timeout duration | | [useDraggableScrollableController](https://api.flutter.dev/flutter/widgets/DraggableScrollableController-class.html) | Creates a `DraggableScrollableController`. | +| [useCarouselController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useCarouselController.html) | Creates and disposes a `CarouselController`. | ## Contributions diff --git a/packages/flutter_hooks/lib/src/carousel_controller.dart b/packages/flutter_hooks/lib/src/carousel_controller.dart new file mode 100644 index 0000000..b1ec352 --- /dev/null +++ b/packages/flutter_hooks/lib/src/carousel_controller.dart @@ -0,0 +1,46 @@ +part of 'hooks.dart'; + +/// Creates a [CarouselController] that will be disposed automatically. +/// +/// See also: +/// - [CarouselController] +CarouselController useCarouselController({ + int initialItem = 0, + List? keys, +}) { + return use( + _CarouselControllerHook( + initialItem: initialItem, + keys: keys, + ), + ); +} + +class _CarouselControllerHook extends Hook { + const _CarouselControllerHook({ + required this.initialItem, + super.keys, + }); + + final int initialItem; + + @override + HookState> createState() => + _CarouselControllerHookState(); +} + +class _CarouselControllerHookState + extends HookState { + late final controller = CarouselController( + initialItem: hook.initialItem, + ); + + @override + CarouselController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'useCarouselController'; +} diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart index 023868d..a7ee1e9 100644 --- a/packages/flutter_hooks/lib/src/hooks.dart +++ b/packages/flutter_hooks/lib/src/hooks.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' show Brightness, + CarouselController, DraggableScrollableController, ExpansionTileController, WidgetStatesController, @@ -17,6 +18,7 @@ import 'framework.dart'; part 'animation.dart'; part 'async.dart'; +part 'carousel_controller.dart'; part 'draggable_scrollable_controller.dart'; part 'expansion_tile_controller.dart'; part 'fixed_extent_scroll_controller.dart'; diff --git a/packages/flutter_hooks/test/carousel_controller_test.dart b/packages/flutter_hooks/test/carousel_controller_test.dart new file mode 100644 index 0000000..b75c90d --- /dev/null +++ b/packages/flutter_hooks/test/carousel_controller_test.dart @@ -0,0 +1,117 @@ +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 'package:flutter_test/flutter_test.dart'; + +import 'mock.dart'; + +void main() { + testWidgets('debugFillProperties', (tester) async { + await tester.pumpWidget( + HookBuilder(builder: (context) { + useCarouselController(); + return const SizedBox(); + }), + ); + + final element = tester.element(find.byType(HookBuilder)); + + expect( + element + .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage) + .toStringDeep(), + equalsIgnoringHashCodes( + 'HookBuilder\n' + ' │ useCarouselController: CarouselController#00000(no clients)\n' + ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n', + ), + ); + }); + + group('useCarouselController', () { + testWidgets('initial values matches with real constructor', (tester) async { + late CarouselController controller; + late CarouselController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = CarouselController(); + controller = useCarouselController(); + return Container(); + }), + ); + + expect(controller.initialItem, controller2.initialItem); + expect(controller.initialScrollOffset, controller2.initialScrollOffset); + expect(controller.keepScrollOffset, controller2.keepScrollOffset); + expect(controller.onAttach, controller2.onAttach); + expect(controller.onDetach, controller2.onDetach); + }); + + testWidgets("returns a CarouselController that doesn't change", + (tester) async { + late CarouselController controller; + late CarouselController controller2; + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller = useCarouselController(); + return Container(); + }), + ); + + expect(controller, isA()); + + await tester.pumpWidget( + HookBuilder(builder: (context) { + controller2 = useCarouselController(); + return Container(); + }), + ); + + expect(identical(controller, controller2), isTrue); + }); + + testWidgets('passes hook parameters to the CarouselController', + (tester) async { + late CarouselController controller; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = useCarouselController( + initialItem: 42, + ); + + return Container(); + }, + ), + ); + + expect(controller.initialItem, 42); + }); + + testWidgets('disposes the CarouselController on unmount', (tester) async { + late CarouselController controller; + + await tester.pumpWidget( + HookBuilder( + builder: (context) { + controller = useCarouselController(); + return Container(); + }, + ), + ); + + // pump another widget so that the old one gets disposed + await tester.pumpWidget(Container()); + + expect( + () => controller.addListener(() {}), + throwsA(isFlutterError.having( + (e) => e.message, 'message', contains('disposed'))), + ); + }); + }); +}