Repository: rrousselGit/flutter_hooks
Pull request #417 (2024-02-25 20:24:40)

add draggable_scrollable_controller
Added useDraggableScrollableController with tests

Didn't edit readme or changelog
Lmk if you want the tests changed

davidmartos96: I'd say this hook would benefit from the changes in https://github.com/rrousselGit/flutter_hooks/pull/415

Current implementation of `useDraggableScrollableController` is not disposing the `DraggableScrollableController`.
dickermoshe: @davidmartos96 Good point!
I copied this from Expandable tile controller. I forgot that that isn't disposed.
I'll fix later

dickermoshe: Fixed, if @rrousselGit plans on merging #415 I'll rebase and push
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/rrousselGit/flutter_hooks/pull/417?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Remi+Rousselet) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.38%. Comparing base [(`15fdb4f`)](https://app.codecov.io/gh/rrousselGit/flutter_hooks/commit/15fdb4f301588276c4d61819e3b978cccf2e1761?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Remi+Rousselet) to head [(`10d35cb`)](https://app.codecov.io/gh/rrousselGit/flutter_hooks/pull/417?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Remi+Rousselet).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #417      +/-   ##
==========================================
+ Coverage   98.36%   98.38%   +0.02%     
==========================================
  Files          21       22       +1     
  Lines         858      869      +11     
==========================================
+ Hits          844      855      +11     
  Misses         14       14              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/rrousselGit/flutter_hooks/pull/417?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Remi+Rousselet).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Remi+Rousselet).

dickermoshe: Done

Patch:
diff --git a/README.md b/README.md
index 424d118..f17108b 100644
--- a/README.md
+++ b/README.md
@@ -19,17 +19,16 @@ logic of say `initState` or `dispose`. An obvious example is `AnimationControlle
 
 ```dart
 class Example extends StatefulWidget {
-  final Duration duration;
+  const Example({super.key, required this.duration});
 
-  const Example({Key? key, required this.duration})
-      : super(key: key);
+  final Duration duration;
 
   @override
   _ExampleState createState() => _ExampleState();
 }
 
 class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
-  AnimationController? _controller;
+  late final AnimationController _controller;
 
   @override
   void initState() {
@@ -41,13 +40,13 @@ class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
   void didUpdateWidget(Example oldWidget) {
     super.didUpdateWidget(oldWidget);
     if (widget.duration != oldWidget.duration) {
-      _controller!.duration = widget.duration;
+      _controller.duration = widget.duration;
     }
   }
 
   @override
   void dispose() {
-    _controller!.dispose();
+    _controller.dispose();
     super.dispose();
   }
 
@@ -74,8 +73,7 @@ This library proposes a third solution:
 
 ```dart
 class Example extends HookWidget {
-  const Example({Key? key, required this.duration})
-      : super(key: key);
+  const Example({super.key, required this.duration});
 
   final Duration duration;
 
@@ -310,12 +308,12 @@ They will take care of creating/updating/disposing an object.
 
 #### dart:async related hooks:
 
-| Name                                                                                                             | Description                                                                   |
-| ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
-| [useStream](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStream.html)                     | Subscribes to a `Stream` and returns its current state as an `AsyncSnapshot`. |
-| [useStreamController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` which will automatically be disposed.            |
-| [useOnStreamChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnStreamChange.html) | Subscribes to a `Stream`, registers handlers, and returns the `StreamSubscription`.
-| [useFuture](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html)                     | Subscribes to a `Future` and returns its current state as an `AsyncSnapshot`. |
+| Name                                                                                                             | Description                                                                         |
+| ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
+| [useStream](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStream.html)                     | Subscribes to a `Stream` and returns its current state as an `AsyncSnapshot`.       |
+| [useStreamController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useStreamController.html) | Creates a `StreamController` which will automatically be disposed.                  |
+| [useOnStreamChange](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useOnStreamChange.html)     | Subscribes to a `Stream`, registers handlers, and returns the `StreamSubscription`. |
+| [useFuture](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFuture.html)                     | Subscribes to a `Future` and returns its current state as an `AsyncSnapshot`.       |
 
 #### Animation related hooks:
 
@@ -338,25 +336,26 @@ They will take care of creating/updating/disposing an object.
 
 A series of hooks with no particular theme.
 
-| Name                                                                                                                                 | Description                                                                |
-| ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
-| [useReducer](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html)                                       | An alternative to `useState` for more complex states.                      |
-| [usePrevious](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html)                                     | Returns the previous argument called to [usePrevious].                     |
-| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html)  | Creates a `TextEditingController`.                                         |
-| [useFocusNode](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html)                                   | Creates a `FocusNode`.                                                     |
-| [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`.                                   |
-| [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`.                         |
-| [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`.                                 |
-| [useWidgetStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useWidgetStatesController.html)     | Creates and disposes a `WidgetStatesController`.                         |
-| [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  |
+| Name                                                                                                                                 | Description                                                                                                                 |
+| ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
+| [useReducer](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useReducer.html)                                       | An alternative to `useState` for more complex states.                                                                       |
+| [usePrevious](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/usePrevious.html)                                     | Returns the previous argument called to [usePrevious].                                                                      |
+| [useTextEditingController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useTextEditingController-constant.html)  | Creates a `TextEditingController`.                                                                                          |
+| [useFocusNode](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useFocusNode.html)                                   | Creates a `FocusNode`.                                                                                                      |
+| [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`.                                                                                    |
+| [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`.                                                                          |
+| [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`.                                                                                  |
+| [useWidgetStatesController](https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useWidgetStatesController.html)         | Creates and disposes a `WidgetStatesController`.                                                                            |
+| [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`.                                                                                  |
 
 ## Contributions
 
diff --git a/packages/flutter_hooks/CHANGELOG.md b/packages/flutter_hooks/CHANGELOG.md
index 149d482..47efce3 100644
--- a/packages/flutter_hooks/CHANGELOG.md
+++ b/packages/flutter_hooks/CHANGELOG.md
@@ -1,3 +1,7 @@
+## Unreleased patch
+
+- Added `useDraggableScrollableController` (@thanks to @dickermoshe)
+
 ## 0.21.1-pre.0
 
 - Bump minimum Flutter SDK to 3.21.0-13.0.pre.4
diff --git a/packages/flutter_hooks/lib/src/draggable_scrollable_controller.dart b/packages/flutter_hooks/lib/src/draggable_scrollable_controller.dart
new file mode 100644
index 0000000..621a339
--- /dev/null
+++ b/packages/flutter_hooks/lib/src/draggable_scrollable_controller.dart
@@ -0,0 +1,34 @@
+part of 'hooks.dart';
+
+/// Creates a [DraggableScrollableController] that will be disposed automatically.
+///
+/// See also:
+/// - [DraggableScrollableController]
+DraggableScrollableController useDraggableScrollableController({
+  List<Object?>? keys,
+}) {
+  return use(_DraggableScrollableControllerHook(keys: keys));
+}
+
+class _DraggableScrollableControllerHook
+    extends Hook<DraggableScrollableController> {
+  const _DraggableScrollableControllerHook({super.keys});
+
+  @override
+  HookState<DraggableScrollableController, Hook<DraggableScrollableController>>
+      createState() => _DraggableScrollableControllerHookState();
+}
+
+class _DraggableScrollableControllerHookState extends HookState<
+    DraggableScrollableController, _DraggableScrollableControllerHook> {
+  final controller = DraggableScrollableController();
+
+  @override
+  String get debugLabel => 'useDraggableScrollableController';
+
+  @override
+  DraggableScrollableController build(BuildContext context) => controller;
+
+  @override
+  void dispose() => controller.dispose();
+}
diff --git a/packages/flutter_hooks/lib/src/hooks.dart b/packages/flutter_hooks/lib/src/hooks.dart
index 859df56..2eef50c 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,
+        DraggableScrollableController,
         ExpansionTileController,
         WidgetStatesController,
         SearchController,
@@ -16,6 +17,7 @@ import 'framework.dart';
 
 part 'animation.dart';
 part 'async.dart';
+part 'draggable_scrollable_controller.dart';
 part 'expansion_tile_controller.dart';
 part 'focus_node.dart';
 part 'focus_scope_node.dart';
diff --git a/packages/flutter_hooks/resources/translations/ko_kr/README.md b/packages/flutter_hooks/resources/translations/ko_kr/README.md
index 2ae0f86..53428f3 100644
--- a/packages/flutter_hooks/resources/translations/ko_kr/README.md
+++ b/packages/flutter_hooks/resources/translations/ko_kr/README.md
@@ -17,17 +17,16 @@
 
 ```dart
 class Example extends StatefulWidget {
-  final Duration duration;
+  const Example({super.key, required this.duration});
 
-  const Example({Key? key, required this.duration})
-      : super(key: key);
+  final Duration duration;
 
   @override
   _ExampleState createState() => _ExampleState();
 }
 
 class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
-  AnimationController? _controller;
+  late final AnimationController _controller;
 
   @override
   void initState() {
@@ -39,13 +38,13 @@ class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
   void didUpdateWidget(Example oldWidget) {
     super.didUpdateWidget(oldWidget);
     if (widget.duration != oldWidget.duration) {
-      _controller!.duration = widget.duration;
+      _controller.duration = widget.duration;
     }
   }
 
   @override
   void dispose() {
-    _controller!.dispose();
+    _controller.dispose();
     super.dispose();
   }
 
@@ -70,8 +69,7 @@ Dart Mixins 으로 이 문제를 해결할 수 있지만, 다른 문제점들이
 
 ```dart
 class Example extends HookWidget {
-  const Example({Key? key, required this.duration})
-      : super(key: key);
+  const Example({super.key, required this.duration});
 
   final Duration duration;
 
diff --git a/packages/flutter_hooks/resources/translations/pt_br/README.md b/packages/flutter_hooks/resources/translations/pt_br/README.md
index 60b787c..761fdf9 100644
--- a/packages/flutter_hooks/resources/translations/pt_br/README.md
+++ b/packages/flutter_hooks/resources/translations/pt_br/README.md
@@ -19,18 +19,16 @@ por exemplo de um `initState` ou `dispose`. Um exemplo é o `AnimationController
 
 ```dart
 class Example extends StatefulWidget {
-  final Duration duration;
+  const Example({super.key, required this.duration});
 
-  const Example({Key? key, @required this.duration})
-      : assert(duration != null),
-        super(key: key);
+  final Duration duration;
 
   @override
   _ExampleState createState() => _ExampleState();
 }
 
 class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
-  AnimationController _controller;
+  late final AnimationController _controller;
 
   @override
   void initState() {
@@ -76,9 +74,7 @@ Essa biblioteca propõe uma terceira solução:
 
 ```dart
 class Example extends HookWidget {
-  const Example({Key? key, @required this.duration})
-      : assert(duration != null),
-        super(key: key);
+  const Example({super.key, required this.duration});
 
   final Duration duration;
 
diff --git a/packages/flutter_hooks/resources/translations/zh_cn/README.md b/packages/flutter_hooks/resources/translations/zh_cn/README.md
index ebd3caf..e33f13d 100644
--- a/packages/flutter_hooks/resources/translations/zh_cn/README.md
+++ b/packages/flutter_hooks/resources/translations/zh_cn/README.md
@@ -20,17 +20,16 @@
 
 ```dart
 class Example extends StatefulWidget {
-  final Duration duration;
+  const Example({super.key, required this.duration});
 
-  const Example({Key? key, required this.duration})
-      : super(key: key);
+  final Duration duration;
 
   @override
   _ExampleState createState() => _ExampleState();
 }
 
 class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
-  AnimationController? _controller;
+  late final AnimationController _controller;
 
   @override
   void initState() {
@@ -42,13 +41,13 @@ class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
   void didUpdateWidget(Example oldWidget) {
     super.didUpdateWidget(oldWidget);
     if (widget.duration != oldWidget.duration) {
-      _controller!.duration = widget.duration;
+      _controller.duration = widget.duration;
     }
   }
 
   @override
   void dispose() {
-    _controller!.dispose();
+    _controller.dispose();
     super.dispose();
   }
 


Test patch:
diff --git a/packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart b/packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart
new file mode 100644
index 0000000..57729b8
--- /dev/null
+++ b/packages/flutter_hooks/test/use_draggable_scrollable_controller_test.dart
@@ -0,0 +1,134 @@
+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) {
+        useDraggableScrollableController();
+        return const SizedBox();
+      }),
+    );
+
+    await tester.pump();
+
+    final element = tester.element(find.byType(HookBuilder));
+
+    expect(
+      element
+          .toDiagnosticsNode(style: DiagnosticsTreeStyle.offstage)
+          .toStringDeep(),
+      equalsIgnoringHashCodes('HookBuilder\n'
+          ' │ useDraggableScrollableController: Instance of\n'
+          " │   'DraggableScrollableController'\n"
+          ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n'),
+    );
+  });
+
+  group('useDraggableScrollableController', () {
+    testWidgets(
+        'controller functions correctly and initial values matches with real constructor',
+        (tester) async {
+      late DraggableScrollableController controller;
+      final controller2 = DraggableScrollableController();
+
+      await tester.pumpWidget(MaterialApp(
+        home: Scaffold(
+          body: HookBuilder(builder: (context) {
+            return Column(
+              children: [
+                ElevatedButton(
+                    onPressed: () {
+                      showBottomSheet(
+                          context: context,
+                          builder: (context) {
+                            // Using a builder here to ensure that the controller is
+                            // disposed when the sheet is closed.
+                            return HookBuilder(builder: (context) {
+                              controller = useDraggableScrollableController();
+                              return DraggableScrollableSheet(
+                                controller: controller,
+                                builder: (context, scrollController) {
+                                  return ListView.builder(
+                                    controller: scrollController,
+                                    itemCount: 100,
+                                    itemBuilder: (context, index) {
+                                      return ListTile(
+                                        title: Text('Item $index on Sheet 1'),
+                                      );
+                                    },
+                                  );
+                                },
+                              );
+                            });
+                          });
+                    },
+                    child: Text("Open Sheet 1")),
+                ElevatedButton(
+                    onPressed: () {
+                      showBottomSheet(
+                          context: context,
+                          builder: (context) {
+                            return DraggableScrollableSheet(
+                              controller: controller2,
+                              builder: (context, scrollController) {
+                                return ListView.builder(
+                                  controller: scrollController,
+                                  itemCount: 100,
+                                  itemBuilder: (context, index) {
+                                    return ListTile(
+                                      title: Text('Item $index on Sheet 2'),
+                                    );
+                                  },
+                                );
+                              },
+                            );
+                          });
+                    },
+                    child: Text("Open Sheet 2"))
+              ],
+            );
+          }),
+        ),
+      ));
+
+      // Open Sheet 1 and get the initial values
+      await tester.tap(find.text('Open Sheet 1'));
+      await tester.pumpAndSettle();
+      final controllerPixels = controller.pixels;
+      final controllerSize = controller.size;
+      final controllerIsAttached = controller.isAttached;
+      // Close Sheet 1 by dragging it down
+      await tester.fling(
+          find.byType(DraggableScrollableSheet), const Offset(0, 500), 300);
+      await tester.pumpAndSettle();
+
+      // Open Sheet 2 and get the initial values
+      await tester.tap(find.text('Open Sheet 2'));
+      await tester.pumpAndSettle();
+      final controller2Pixels = controller2.pixels;
+      final controller2Size = controller2.size;
+      final controller2IsAttached = controller2.isAttached;
+      // Close Sheet 2 by dragging it down
+      await tester.fling(
+          find.byType(DraggableScrollableSheet), const Offset(0, 500), 300);
+      await tester.pumpAndSettle();
+
+      // Compare the initial values of the two controllers
+      expect(controllerPixels, controller2Pixels);
+      expect(controllerSize, controller2Size);
+      expect(controllerIsAttached, controller2IsAttached);
+
+      // Open Sheet 1 again and use the controller to scroll
+      await tester.tap(find.text('Open Sheet 1'));
+      await tester.pumpAndSettle();
+      const targetSize = 1.0;
+      controller.jumpTo(targetSize);
+      expect(targetSize, controller.size);
+    });
+  });
+}