Repository: rrousselGit/flutter_hooks (dart)

Issue:
useFuture/useStream: does initialData need to be required?
At 0.16.0 with nullsafety useFuture gets `required T initialData` parameter:
https://github.com/rrousselGit/flutter_hooks/blob/2b02b99ce6b327fd3a6df6f9c52cb2fe791600f6/lib/src/async.dart#L11-L15

This means when there is no initial data, one have to explicitly pass null:
```dart
final snapshot = useFuture(future, initialData: null);
```

Are there any benefits of making it required?

Pull request:
Fixes #229 
As discussed, this now uses the strategy followed by the Flutter framework in `FutureBuilder` and `StreamBuilder`.
https://github.com/rrousselGit/flutter_hooks/issues/229#issuecomment-843835771

Patch:
diff --git a/packages/flutter_hooks/lib/src/async.dart b/packages/flutter_hooks/lib/src/async.dart
index 71e00b79..90c1809d 100644
--- a/packages/flutter_hooks/lib/src/async.dart
+++ b/packages/flutter_hooks/lib/src/async.dart
@@ -8,7 +8,7 @@ part of 'hooks.dart';
 /// See also:
 ///   * [Future], the listened object.
 ///   * [useStream], similar to [useFuture] but for [Stream].
-AsyncSnapshot<T?> useFuture<T>(
+AsyncSnapshot<T> useFuture<T>(
   Future<T>? future, {
   T? initialData,
   bool preserveState = true,
@@ -22,7 +22,7 @@ AsyncSnapshot<T?> useFuture<T>(
   );
 }
 
-class _FutureHook<T> extends Hook<AsyncSnapshot<T?>> {
+class _FutureHook<T> extends Hook<AsyncSnapshot<T>> {
   const _FutureHook(
     this.future, {
     required this.initialData,
@@ -37,13 +37,16 @@ class _FutureHook<T> extends Hook<AsyncSnapshot<T?>> {
   _FutureStateHook<T> createState() => _FutureStateHook<T>();
 }
 
-class _FutureStateHook<T> extends HookState<AsyncSnapshot<T?>, _FutureHook<T>> {
+class _FutureStateHook<T> extends HookState<AsyncSnapshot<T>, _FutureHook<T>> {
   /// An object that identifies the currently active callbacks. Used to avoid
   /// calling setState from stale callbacks, e.g. after disposal of this state,
   /// or after widget reconfiguration to a new Future.
   Object? _activeCallbackIdentity;
-  late AsyncSnapshot<T?> _snapshot =
-      AsyncSnapshot<T?>.withData(ConnectionState.none, hook.initialData);
+  late AsyncSnapshot<T> _snapshot = initial;
+
+  AsyncSnapshot<T> get initial => hook.initialData == null
+      ? AsyncSnapshot<T>.nothing()
+      : AsyncSnapshot<T>.withData(ConnectionState.none, hook.initialData as T);
 
   @override
   void initHook() {
@@ -60,8 +63,7 @@ class _FutureStateHook<T> extends HookState<AsyncSnapshot<T?>, _FutureHook<T>> {
         if (hook.preserveState) {
           _snapshot = _snapshot.inState(ConnectionState.none);
         } else {
-          _snapshot = AsyncSnapshot<T?>.withData(
-              ConnectionState.none, hook.initialData);
+          _snapshot = initial;
         }
       }
       _subscribe();
@@ -80,14 +82,14 @@ class _FutureStateHook<T> extends HookState<AsyncSnapshot<T?>, _FutureHook<T>> {
       hook.future!.then<void>((data) {
         if (_activeCallbackIdentity == callbackIdentity) {
           setState(() {
-            _snapshot = AsyncSnapshot<T?>.withData(ConnectionState.done, data);
+            _snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
           });
         }
         // ignore: avoid_types_on_closure_parameters
       }, onError: (Object error, StackTrace stackTrace) {
         if (_activeCallbackIdentity == callbackIdentity) {
           setState(() {
-            _snapshot = AsyncSnapshot<T?>.withError(
+            _snapshot = AsyncSnapshot<T>.withError(
               ConnectionState.done,
               error,
               stackTrace,
@@ -104,7 +106,7 @@ class _FutureStateHook<T> extends HookState<AsyncSnapshot<T?>, _FutureHook<T>> {
   }
 
   @override
-  AsyncSnapshot<T?> build(BuildContext context) {
+  AsyncSnapshot<T> build(BuildContext context) {
     return _snapshot;
   }
 
@@ -123,7 +125,7 @@ class _FutureStateHook<T> extends HookState<AsyncSnapshot<T?>, _FutureHook<T>> {
 /// See also:
 ///   * [Stream], the object listened.
 ///   * [useFuture], similar to [useStream] but for [Future].
-AsyncSnapshot<T?> useStream<T>(
+AsyncSnapshot<T> useStream<T>(
   Stream<T>? stream, {
   T? initialData,
   bool preserveState = true,
@@ -137,7 +139,7 @@ AsyncSnapshot<T?> useStream<T>(
   );
 }
 
-class _StreamHook<T> extends Hook<AsyncSnapshot<T?>> {
+class _StreamHook<T> extends Hook<AsyncSnapshot<T>> {
   const _StreamHook(
     this.stream, {
     required this.initialData,
@@ -153,9 +155,9 @@ class _StreamHook<T> extends Hook<AsyncSnapshot<T?>> {
 }
 
 /// a clone of [StreamBuilderBase] implementation
-class _StreamHookState<T> extends HookState<AsyncSnapshot<T?>, _StreamHook<T>> {
+class _StreamHookState<T> extends HookState<AsyncSnapshot<T>, _StreamHook<T>> {
   StreamSubscription<T>? _subscription;
-  late AsyncSnapshot<T?> _summary = initial;
+  late AsyncSnapshot<T> _summary = initial;
 
   @override
   void initHook() {
@@ -210,32 +212,33 @@ class _StreamHookState<T> extends HookState<AsyncSnapshot<T?>, _StreamHook<T>> {
   }
 
   @override
-  AsyncSnapshot<T?> build(BuildContext context) {
+  AsyncSnapshot<T> build(BuildContext context) {
     return _summary;
   }
 
-  AsyncSnapshot<T?> get initial =>
-      AsyncSnapshot<T?>.withData(ConnectionState.none, hook.initialData);
+  AsyncSnapshot<T> get initial => hook.initialData == null
+      ? AsyncSnapshot<T>.nothing()
+      : AsyncSnapshot<T>.withData(ConnectionState.none, hook.initialData as T);
 
-  AsyncSnapshot<T?> afterConnected(AsyncSnapshot<T?> current) =>
+  AsyncSnapshot<T> afterConnected(AsyncSnapshot<T> current) =>
       current.inState(ConnectionState.waiting);
 
-  AsyncSnapshot<T?> afterData(T data) {
-    return AsyncSnapshot<T?>.withData(ConnectionState.active, data);
+  AsyncSnapshot<T> afterData(T data) {
+    return AsyncSnapshot<T>.withData(ConnectionState.active, data);
   }
 
-  AsyncSnapshot<T?> afterError(Object error, StackTrace stackTrace) {
-    return AsyncSnapshot<T?>.withError(
+  AsyncSnapshot<T> afterError(Object error, StackTrace stackTrace) {
+    return AsyncSnapshot<T>.withError(
       ConnectionState.active,
       error,
       stackTrace,
     );
   }
 
-  AsyncSnapshot<T?> afterDone(AsyncSnapshot<T?> current) =>
+  AsyncSnapshot<T> afterDone(AsyncSnapshot<T> current) =>
       current.inState(ConnectionState.done);
 
-  AsyncSnapshot<T?> afterDisconnected(AsyncSnapshot<T?> current) =>
+  AsyncSnapshot<T> afterDisconnected(AsyncSnapshot<T> current) =>
       current.inState(ConnectionState.none);
 
   @override


Test patch:
diff --git a/packages/flutter_hooks/test/use_future_test.dart b/packages/flutter_hooks/test/use_future_test.dart
index 9af667a6..497f98de 100644
--- a/packages/flutter_hooks/test/use_future_test.dart
+++ b/packages/flutter_hooks/test/use_future_test.dart
@@ -9,10 +9,10 @@ import 'mock.dart';
 void main() {
   testWidgets('default preserve state, changing future keeps previous value',
       (tester) async {
-    late AsyncSnapshot<int?> value;
-    Widget Function(BuildContext) builder(Future<int?> stream) {
+    late AsyncSnapshot<int> value;
+    Widget Function(BuildContext) builder(Future<int> stream) {
       return (context) {
-        value = useFuture<int?>(stream);
+        value = useFuture(stream);
         return Container();
       };
     }
@@ -50,7 +50,7 @@ void main() {
           .toStringDeep(),
       equalsIgnoringHashCodes(
         'HookBuilder\n'
-        ' │ useFuture: AsyncSnapshot<int?>(ConnectionState.done, 42, null,\n'
+        ' │ useFuture: AsyncSnapshot<int>(ConnectionState.done, 42, null,\n'
         ' │   null)\n'
         ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n',
       ),
@@ -59,10 +59,10 @@ void main() {
 
   testWidgets('If preserveState == false, changing future resets value',
       (tester) async {
-    late AsyncSnapshot<int?> value;
-    Widget Function(BuildContext) builder(Future<int?> stream) {
+    late AsyncSnapshot<int> value;
+    Widget Function(BuildContext) builder(Future<int> stream) {
       return (context) {
-        value = useFuture<int?>(stream, preserveState: false);
+        value = useFuture(stream, preserveState: false);
         return Container();
       };
     }
diff --git a/packages/flutter_hooks/test/use_stream_test.dart b/packages/flutter_hooks/test/use_stream_test.dart
index f5ae7c80..f48b9146 100644
--- a/packages/flutter_hooks/test/use_stream_test.dart
+++ b/packages/flutter_hooks/test/use_stream_test.dart
@@ -31,7 +31,7 @@ void main() {
           .toStringDeep(),
       equalsIgnoringHashCodes(
         'HookBuilder\n'
-        ' │ useStream: AsyncSnapshot<int?>(ConnectionState.done, 42, null,\n'
+        ' │ useStream: AsyncSnapshot<int>(ConnectionState.done, 42, null,\n'
         ' │   null)\n'
         ' └SizedBox(renderObject: RenderConstrainedBox#00000)\n',
       ),
@@ -40,10 +40,10 @@ void main() {
 
   testWidgets('default preserve state, changing stream keeps previous value',
       (tester) async {
-    late AsyncSnapshot<int?>? value;
-    Widget Function(BuildContext) builder(Stream<int?> stream) {
+    late AsyncSnapshot<int>? value;
+    Widget Function(BuildContext) builder(Stream<int> stream) {
       return (context) {
-        value = useStream<int?>(stream);
+        value = useStream(stream);
         return Container();
       };
     }
@@ -62,10 +62,10 @@ void main() {
   });
   testWidgets('If preserveState == false, changing stream resets value',
       (tester) async {
-    late AsyncSnapshot<int?>? value;
-    Widget Function(BuildContext) builder(Stream<int?> stream) {
+    late AsyncSnapshot<int>? value;
+    Widget Function(BuildContext) builder(Stream<int> stream) {
       return (context) {
-        value = useStream<int?>(stream, preserveState: false);
+        value = useStream(stream, preserveState: false);
         return Container();
       };
     }
@@ -98,7 +98,7 @@ void main() {
         .pumpWidget(HookBuilder(builder: snapshotText(controllerA.stream)));
     expect(
         find.text(
-          'AsyncSnapshot<String?>(ConnectionState.waiting, , null, null)',
+          'AsyncSnapshot<String>(ConnectionState.waiting, , null, null)',
         ),
         findsOneWidget);
     await tester
@@ -108,7 +108,7 @@ void main() {
     await eventFiring(tester);
     expect(
         find.text(
-            'AsyncSnapshot<String?>(ConnectionState.active, B, null, null)'),
+            'AsyncSnapshot<String>(ConnectionState.active, B, null, null)'),
         findsOneWidget);
   });
   testWidgets('tracks events and errors of stream until completion',
@@ -118,14 +118,14 @@ void main() {
         .pumpWidget(HookBuilder(builder: snapshotText(controller.stream)));
     expect(
         find.text(
-          'AsyncSnapshot<String?>(ConnectionState.waiting, , null, null)',
+          'AsyncSnapshot<String>(ConnectionState.waiting, , null, null)',
         ),
         findsOneWidget);
     controller..add('1')..add('2');
     await eventFiring(tester);
     expect(
         find.text(
-          'AsyncSnapshot<String?>(ConnectionState.active, 2, null, null)',
+          'AsyncSnapshot<String>(ConnectionState.active, 2, null, null)',
         ),
         findsOneWidget);
     controller
@@ -136,7 +136,7 @@ void main() {
 
     expect(
       find.text(
-        'AsyncSnapshot<String?>(ConnectionState.active, null, bad, stackTrace)',
+        'AsyncSnapshot<String>(ConnectionState.active, null, bad, stackTrace)',
       ),
       findsOneWidget,
     );
@@ -146,7 +146,7 @@ void main() {
     await eventFiring(tester);
 
     expect(
-      find.text('AsyncSnapshot<String?>(ConnectionState.done, 4, null, null)'),
+      find.text('AsyncSnapshot<String>(ConnectionState.done, 4, null, null)'),
       findsOneWidget,
     );
   });
@@ -160,7 +160,7 @@ void main() {
 
     expect(
       find.text(
-          'AsyncSnapshot<String?>(ConnectionState.waiting, I, null, null)'),
+          'AsyncSnapshot<String>(ConnectionState.waiting, I, null, null)'),
       findsOneWidget,
     );
   });
@@ -173,7 +173,7 @@ void main() {
 
     expect(
       find.text(
-          'AsyncSnapshot<String?>(ConnectionState.waiting, I, null, null)'),
+          'AsyncSnapshot<String>(ConnectionState.waiting, I, null, null)'),
       findsOneWidget,
     );
 
@@ -185,7 +185,7 @@ void main() {
 
     expect(
       find.text(
-        'AsyncSnapshot<String?>(ConnectionState.waiting, I, null, null)',
+        'AsyncSnapshot<String>(ConnectionState.waiting, I, null, null)',
       ),
       findsOneWidget,
     );