Repository: rrousselGit/flutter_hooks (dart)

Issue:
Add hook "useFixedExtentScrollController"
**Is your feature request related to a problem? Please describe.**
Add a hook for FixedExtentScrollController

**Describe the solution you'd like**
```dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

/// Creates [FixedExtentScrollController] that will be disposed automatically.
///
/// See also:
/// - [FixedExtentScrollController]
FixedExtentScrollController useFixedExtentScrollController({
  int initialItem = 0,
  List<Object?>? keys,
}) {
  return use(
    _FixedExtentScrollControllerHook(
      initialItem: initialItem,
      keys: keys,
    ),
  );
}

class _FixedExtentScrollControllerHook
    extends Hook<FixedExtentScrollController> {
  const _FixedExtentScrollControllerHook({
    required this.initialItem,
    super.keys,
  });

  final int initialItem;

  @override
  HookState<FixedExtentScrollController, Hook<FixedExtentScrollController>>
      createState() => _FixedExtentScrollControllerHookState();
}

class _FixedExtentScrollControllerHookState extends HookState<
    FixedExtentScrollController, _FixedExtentScrollControllerHook> {
  late final controller = FixedExtentScrollController(
    initialItem: hook.initialItem,
  );

  @override
  FixedExtentScrollController build(BuildContext context) => controller;

  @override
  void dispose() => controller.dispose();

  @override
  String get debugLabel => 'useFixedExtentScrollController';
}

```

**Additional context**

Example:
```dart
ListWheelScrollView(
  controller: scrollController,
  itemExtent: 25,
  squeeze: 0.8,
  onSelectedItemChanged: (value) {
    selected.value = value;
  },
  children: counts.map(
       (index) => OnClick(
           onTap: () {
                 selected.value = index;
                 scrollController.jumpToItem(index);
            },
```
https://dartpad.dev/?id=a8d241f3fcc2f73a43b77860e6b5f066

Pull request description:
Fixes #433 

I also adjusted the tests I added in #436 to better match the existing test structure :)

Patch:
diff --git a/README.md b/README.md
index 984f33b..99aff23 100644
--- a/README.md
+++ b/README.md
@@ -346,6 +346,7 @@ A series of hooks with no particular theme.
 | [useTabController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTabController.html)                           | Creates and disposes a `TabController`.                                                                                     |
 | [useScrollController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useScrollController.html)                     | Creates and disposes a `ScrollController`.                                                                                  |
 | [usePageController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePageController.html)                         | Creates and disposes a `PageController`.                                                                                    |
+| [useFixedExtentScrollController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFixedExtentScrollController.html)                         | Creates and disposes a `FixedExtentScrollController`.                                                                                    |
 | [useAppLifecycleState](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAppLifecycleState.html)                   | Returns the current `AppLifecycleState` and rebuilds the widget on change.                                                  |
 | [useOnAppLifecycleStateChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnAppLifecycleStateChange.html)   | Listens to `AppLifecycleState` changes and triggers a callback on change.                                                   |
 | [useTransformationController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTransformationController.html)     | Creates and disposes a `TransformationController`.                                                                          |
diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md
index 90c4b44..6835524 100644
--- a/packages/flutter_hooks/CHANGELOG.md
+++ b/packages/flutter_hooks/CHANGELOG.md
@@ -1,4 +1,9 @@
+## Unreleased build
+
+- Added `useFixedExtentScrollController` (thanks to @whynotmake-it)
+
 ## 0.21.1-pre.3 - 2024-07-30
+
 - Added `useOnListenableChange` (thanks to @whynotmake-it)
 
 ## 0.21.1-pre.2 - 2024-07-22
diff --git a/packages/flutter_hooks/lib/src/fixed_extent_scroll_controller.dart b/packages/flutter_hooks/lib/src/fixed_extent_scroll_controller.dart
new file mode 100644
index 0000000..a70a161
--- /dev/null
+++ b/packages/flutter_hooks/lib/src/fixed_extent_scroll_controller.dart
@@ -0,0 +1,57 @@
+part of 'hooks.dart';
+
+/// Creates [FixedExtentScrollController] that will be disposed automatically.
+///
+/// See also:
+/// - [FixedExtentScrollController]
+FixedExtentScrollController useFixedExtentScrollController({
+  int initialItem = 0,
+  ScrollControllerCallback? onAttach,
+  ScrollControllerCallback? onDetach,
+  List<Object?>? keys,
+}) {
+  return use(
+    _FixedExtentScrollControllerHook(
+      initialItem: initialItem,
+      onAttach: onAttach,
+      onDetach: onDetach,
+      keys: keys,
+    ),
+  );
+}
+
+class _FixedExtentScrollControllerHook
+    extends Hook<FixedExtentScrollController> {
+  const _FixedExtentScrollControllerHook({
+    required this.initialItem,
+    this.onAttach,
+    this.onDetach,
+    super.keys,
+  });
+
+  final int initialItem;
+  final ScrollControllerCallback? onAttach;
+  final ScrollControllerCallback? onDetach;
+
+  @override
+  HookState<FixedExtentScrollController, Hook<FixedExtentScrollController>>
+      createState() => _FixedExtentScrollControllerHookState();
+}
+
+class _FixedExtentScrollControllerHookState extends HookState<
+    FixedExtentScrollController, _FixedExtentScrollControllerHook> {
+  late final controller = FixedExtentScrollController(
+    initialItem: hook.initialItem,
+    onAttach: hook.onAttach,
+    onDetach: hook.onDetach,
+  );
+
+  @override
+  FixedExtentScrollController build(BuildContext context) => controller;
+
+  @override
+  void dispose() => controller.dispose();
+
+  @override
+  String get debugLabel => 'useFixedExtentScrollController';
+}
diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart
index 2eef50c..023868d 100644
--- a/packages/flutter_hooks/lib/src/hooks.dart
+++ b/packages/flutter_hooks/lib/src/hooks.dart
@@ -19,6 +19,7 @@ part 'animation.dart';
 part 'async.dart';
 part 'draggable_scrollable_controller.dart';
 part 'expansion_tile_controller.dart';
+part 'fixed_extent_scroll_controller.dart';
 part 'focus_node.dart';
 part 'focus_scope_node.dart';
 part 'keep_alive.dart';


Test patch:
diff --git a/packages/flutter_hooks/test/use_fixed_extent_scroll_controller_test.dart b/packages/flutter_hooks/test/use_fixed_extent_scroll_controller_test.dart
new file mode 100644
index 0000000..00f9f6c
--- /dev/null
+++ b/packages/flutter_hooks/test/use_fixed_extent_scroll_controller_test.dart
@@ -0,0 +1,102 @@
+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) {
+        useFixedExtentScrollController();
+        return const SizedBox();
+      }),
+    );
+
+    final element = tester.element(find.byType(HookBuilder));
+
+    expect(
+      element
+          .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage)
+          .toStringDeep(),
+      equalsIgnoringHashCodes(
+        'HookBuilder\n'
+        ' │ useFixedExtentScrollController:\n'
+        ' │   FixedExtentScrollController#00000(no clients)\n'
+        ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n',
+      ),
+    );
+  });
+
+  group('useFixedExtentScrollController', () {
+    testWidgets('initial values matches with real constructor', (tester) async {
+      late FixedExtentScrollController controller;
+      late FixedExtentScrollController controller2;
+
+      await tester.pumpWidget(
+        HookBuilder(builder: (context) {
+          controller2 = FixedExtentScrollController();
+          controller = useFixedExtentScrollController();
+          return Container();
+        }),
+      );
+
+      expect(controller.debugLabel, controller2.debugLabel);
+      expect(controller.initialItem, controller2.initialItem);
+      expect(controller.onAttach, controller2.onAttach);
+      expect(controller.onDetach, controller2.onDetach);
+    });
+    testWidgets("returns a FixedExtentScrollController that doesn't change",
+        (tester) async {
+      late FixedExtentScrollController controller;
+      late FixedExtentScrollController controller2;
+
+      await tester.pumpWidget(
+        HookBuilder(builder: (context) {
+          controller2 = FixedExtentScrollController();
+          controller = useFixedExtentScrollController();
+          return Container();
+        }),
+      );
+      expect(controller, isA<FixedExtentScrollController>());
+
+      await tester.pumpWidget(
+        HookBuilder(builder: (context) {
+          controller2 = useFixedExtentScrollController();
+          return Container();
+        }),
+      );
+
+      expect(identical(controller, controller2), isTrue);
+    });
+
+    testWidgets('passes hook parameters to the FixedExtentScrollController',
+        (tester) async {
+      late FixedExtentScrollController controller;
+
+      void onAttach(ScrollPosition position) {}
+      void onDetach(ScrollPosition position) {}
+
+      await tester.pumpWidget(
+        HookBuilder(
+          builder: (context) {
+            controller = useFixedExtentScrollController(
+              initialItem: 42,
+              onAttach: onAttach,
+              onDetach: onDetach,
+            );
+
+            return Container();
+          },
+        ),
+      );
+
+      expect(controller.initialItem, 42);
+      expect(controller.onAttach, onAttach);
+      expect(controller.onDetach, onDetach);
+    });
+  });
+}
+
+class TickerProviderMock extends Mock implements TickerProvider {}
diff --git a/packages/flutter_hooks/test/use_page_controller_test.dart b/packages/flutter_hooks/test/use_page_controller_test.dart
index 31f0cf9..7015090 100644
--- a/packages/flutter_hooks/test/use_page_controller_test.dart
+++ b/packages/flutter_hooks/test/use_page_controller_test.dart
@@ -45,6 +45,8 @@ void main() {
       expect(controller.initialPage, controller2.initialPage);
       expect(controller.keepPage, controller2.keepPage);
       expect(controller.viewportFraction, controller2.viewportFraction);
+      expect(controller.onAttach, controller2.onAttach);
+      expect(controller.onDetach, controller2.onDetach);
     });
     testWidgets("returns a PageController that doesn't change", (tester) async {
       late PageController controller;
@@ -98,23 +100,6 @@ void main() {
       expect(controller.onDetach, onDetach);
     });
 
-    testWidgets('onAttach and onDetach are null by default', (tester) async {
-      late PageController controller;
-
-      await tester.pumpWidget(
-        HookBuilder(
-          builder: (context) {
-            controller = usePageController();
-
-            return Container();
-          },
-        ),
-      );
-
-      expect(controller.onAttach, isNull);
-      expect(controller.onDetach, isNull);
-    });
-
     testWidgets('disposes the PageController on unmount', (tester) async {
       late PageController controller;
 
diff --git a/packages/flutter_hooks/test/use_scroll_controller_test.dart b/packages/flutter_hooks/test/use_scroll_controller_test.dart
index 3ab90b2..fee5fd5 100644
--- a/packages/flutter_hooks/test/use_scroll_controller_test.dart
+++ b/packages/flutter_hooks/test/use_scroll_controller_test.dart
@@ -44,6 +44,8 @@ void main() {
       expect(controller.debugLabel, controller2.debugLabel);
       expect(controller.initialScrollOffset, controller2.initialScrollOffset);
       expect(controller.keepScrollOffset, controller2.keepScrollOffset);
+      expect(controller.onAttach, controller2.onAttach);
+      expect(controller.onDetach, controller2.onDetach);
     });
     testWidgets("returns a ScrollController that doesn't change",
         (tester) async {
@@ -98,22 +100,6 @@ void main() {
       expect(controller.onAttach, onAttach);
       expect(controller.onDetach, onDetach);
     });
-
-    testWidgets('onAttach and onDetach are null by default', (tester) async {
-      late ScrollController controller;
-
-      await tester.pumpWidget(
-        HookBuilder(
-          builder: (context) {
-            controller = useScrollController();
-
-            return Container();
-          },
-        ),
-      );
-      expect(controller.onAttach, isNull);
-      expect(controller.onDetach, isNull);
-    });
   });
 }